Repository: desugar-64/imla
Branch: main
Commit: 14fd51c388e1
Files: 116
Total size: 677.3 KB
Directory structure:
gitextract_8x6yvds7/
├── .gitignore
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── dev/
│ │ └── serhiiyaremych/
│ │ └── imla/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── assets/
│ │ │ └── loremipsum.json
│ │ ├── java/
│ │ │ └── dev/
│ │ │ └── serhiiyaremych/
│ │ │ └── imla/
│ │ │ ├── DemoApp.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── data/
│ │ │ │ ├── ApiClient.kt
│ │ │ │ ├── Poll.kt
│ │ │ │ └── UserPost.kt
│ │ │ └── ui/
│ │ │ ├── theme/
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ └── userpost/
│ │ │ ├── PollView.kt
│ │ │ ├── PostImageView.kt
│ │ │ ├── SimpleImageViewer.kt
│ │ │ └── UserPostView.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ └── ic_launcher_background.xml
│ │ ├── drawable-v24/
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── values/
│ │ │ ├── colors.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ └── xml/
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test/
│ └── java/
│ └── dev/
│ └── serhiiyaremych/
│ └── imla/
│ └── ExampleUnitTest.kt
├── benchmark/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── dev/
│ └── serhiiyaremych/
│ └── benchmark/
│ └── ImlaBenchmark.kt
├── build.gradle.kts
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── imla/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── dev/
│ │ └── serhiiyaremych/
│ │ └── imla/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── assets/
│ │ │ └── shader/
│ │ │ ├── blur_down.frag
│ │ │ ├── blur_quad.frag
│ │ │ ├── blur_up.frag
│ │ │ ├── default_quad.frag
│ │ │ ├── default_quad.vert
│ │ │ ├── external_quad.frag
│ │ │ ├── mask.frag
│ │ │ ├── noise.frag
│ │ │ ├── simple_blur.frag
│ │ │ ├── simple_ext_quad.frag
│ │ │ ├── simple_mask.frag
│ │ │ ├── simple_quad.frag
│ │ │ └── simple_quad.vert
│ │ └── java/
│ │ └── dev/
│ │ └── serhiiyaremych/
│ │ └── imla/
│ │ ├── ext/
│ │ │ └── util.kt
│ │ ├── modifier/
│ │ │ └── ImlaBlurSourceModifier.kt
│ │ ├── renderer/
│ │ │ ├── GfxBuffer.kt
│ │ │ ├── RenderCommand.kt
│ │ │ ├── Renderer2D.kt
│ │ │ ├── RendererApi.kt
│ │ │ ├── SimpleRenderer.kt
│ │ │ ├── SubTexture2D.kt
│ │ │ ├── Texture.kt
│ │ │ ├── VertexArray.kt
│ │ │ ├── camera/
│ │ │ │ ├── OrthographicCamera.kt
│ │ │ │ └── OrthographicCameraController.kt
│ │ │ ├── framebuffer/
│ │ │ │ ├── BumpAllocatorPool.kt
│ │ │ │ ├── FrameBuffer.kt
│ │ │ │ └── FramebufferPool.kt
│ │ │ ├── objects/
│ │ │ │ └── QuadShaderProgram.kt
│ │ │ ├── opengl/
│ │ │ │ ├── OpenGLRendererApi.kt
│ │ │ │ ├── OpenGLShader.kt
│ │ │ │ ├── OpenGLTexture2D.kt
│ │ │ │ ├── OpenGLUniformBuffer.kt
│ │ │ │ ├── OpenGLVertexArray.kt
│ │ │ │ └── buffer/
│ │ │ │ ├── OpenGLFrameBuffer.kt
│ │ │ │ ├── OpenGLIndexBuffer.kt
│ │ │ │ └── OpenGLVertexBuffer.kt
│ │ │ ├── primitive/
│ │ │ │ └── QuadVertex.kt
│ │ │ ├── shader/
│ │ │ │ ├── Shader.kt
│ │ │ │ ├── ShaderBinder.kt
│ │ │ │ ├── ShaderLibrary.kt
│ │ │ │ └── ShaderProgram.kt
│ │ │ ├── stats/
│ │ │ │ └── ShaderStats.kt
│ │ │ └── util/
│ │ │ └── SizeUtil.kt
│ │ ├── ui/
│ │ │ └── BackdropBlur.kt
│ │ └── uirenderer/
│ │ ├── MaskTextureRenderer.kt
│ │ ├── RenderObject.kt
│ │ ├── RenderableRootLayer.kt
│ │ ├── RenderingPipeline.kt
│ │ ├── Style.kt
│ │ ├── UiLayerRenderer.kt
│ │ └── processing/
│ │ ├── EffectCoordinator.kt
│ │ ├── EffectsHolder.kt
│ │ ├── PostProcessingEffect.kt
│ │ ├── SimpleQuadRenderer.kt
│ │ ├── blend/
│ │ │ └── PostBlendEffect.kt
│ │ ├── blur/
│ │ │ ├── BlurContext.kt
│ │ │ ├── DualBlurEffect.kt
│ │ │ ├── DualBlurFilterShaderProgram.kt
│ │ │ ├── SepGaussianBlurEffect.kt
│ │ │ └── SimpleBlurShaderProgram.kt
│ │ ├── mask/
│ │ │ ├── MaskEffect.kt
│ │ │ └── MaskShaderProgram.kt
│ │ ├── noise/
│ │ │ ├── NoiseEffect.kt
│ │ │ └── NoiseShaderProgram.kt
│ │ └── preprocess/
│ │ └── PreProcessFilter.kt
│ └── test/
│ └── java/
│ └── dev/
│ └── serhiiyaremych/
│ └── imla/
│ └── ExampleUnitTest.kt
└── settings.gradle.kts
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Serhii Yaremych
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Imla - (Experimental) GPU-Accelerated Blurring for Android Jetpack Compose UI
> ⚠️ **Disclaimer**:
> This project is experimental and not intended for use in production applications.
## Description
Imla (Ukrainian for "Haze", pronounced [ˈimlɑ] (eem-lah)) is an experimental project exploring
GPU-accelerated view blurring on Android. It aims to implement efficient blurring effects using
OpenGL, targeting devices from Android 6 (API 23) onwards.
The project serves as a playground for experimenting with GPU rendering and post-processing effects,
with the potential to evolve into a full-fledged library in the future.
## Features
- Gamma corrected blurring;
- Adjustable blur radius;
- Color tinting of blurred areas;
- Blending with a noise mask for a frosted glass effect;
- Setting blurring masks for gradient blur effects;
- Supports Android 6 (API 23) onwards.
## Demo
| **Nexus 5** |
|--------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
|
|
|
|
## How It Works
Imla uses a combination of `GraphicsLayer` from Jetpack Compose and OpenGL ES 3.0 to achieve fast,
GPU-accelerated blurring. The processing pipeline does multiple steps to achieve blurred effect:
1. A specified view is rendered as a background texture using `Surface` and `SurfaceTexture`(
see [RenderableRootLayer.kt](imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/RenderableRootLayer.kt)).
2. The rendered texture is copied to a post-processing framebuffer.
3. The [BackdropBlur](imla/src/main/java/dev/serhiiyaremych/imla/ui/BackdropBlur.kt) composable
wraps
child composable elements that need a blurred background.
4. The blurred texture is rendered as a SurfaceView background to the wrapped elements, creating the
illusion of a blurred backdrop.
The post-processing pipeline includes:
1. Down-sampling the background
texture, [RenderableRootLayer.kt](imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/RenderableRootLayer.kt);
2. Applying a two-pass blur algorithm with gamma
correction, [BlurEffect](imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/blur/BlurEffect.kt);
3. Blending with a noise texture for a frosted glass
effect, [NoiseEffect](imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/noise/NoiseEffect.kt);
4. (Optional) Application of a mask for progressive or gradient blur
effects, [MaskEffect](imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/mask/MaskEffect.kt).
Importantly, all blur color processing is performed in the linear color space, with appropriate
gamma decoding and encoding applied to ensure colors blend naturally, preserving vibrancy and
contrast.
## Rendering Abstraction
The project reuses the OpenGL abstractions from another experimental
project: [desugar-64/android-opengl-renderer](https://github.com/desugar-64/android-opengl-renderer).
This repo is a playground to learn graphics and OpenGL, including some convenient abstractions
for setting up OpenGL data structures and calling various OpenGL functions.
The current implementation uses a fully dynamic renderer, which pushes vertex data each frame. While
this approach offers flexibility, it introduces some performance overhead. Future iterations aim to
optimize this aspect of the rendering pipeline.
## Performance
Current performance metrics for the blur effect on a Pixel 6 device:
- `BlurEffect#applyEffect`: ~1.19ms
- `RenderObject#onRender` : ~4.842ms
| Trace |
|--------------------------------------------------------------------------------------------------------|
|  |
|  |
|  |
These timings indicate that the blur effect and rendering process are relatively fast, but there's
still room for optimization.
## Future Plans
- [x] Implement Dual Kawase Blurring Filter for improved performance;
- [ ] Optimize the rendering pipeline and OpenGL abstractions;
- [ ] Address synchronization issues between the main thread and OpenGL thread.
## Contributing
This project is open to suggestions and contributions. Feel free to open issues
or submit pull requests on GitHub.
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
## Development Updates
For project development updates and history, refer
to [this Twitter thread](https://x.com/desugar_64/status/1787633739117277669).
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/build.gradle.kts
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.jetbrains.kotlin.serialization)
}
android {
namespace = "dev.serhiiyaremych.imla"
compileSdk = 35
defaultConfig {
applicationId = "dev.serhiiyaremych.imla"
minSdk = 23
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
create("benchmark") {
initWith(buildTypes.getByName("release"))
matchingFallbacks += listOf("release")
isDebuggable = false
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs += "-Xcontext-receivers"
}
buildFeatures {
compose = true
shaders = true
dataBinding = false
viewBinding = false
mlModelBinding = false
aidl = false
buildConfig = false
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
lint {
abortOnError = true
warningsAsErrors = true
}
}
dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.haze)
implementation(libs.androidx.runtime.tracing)
implementation(libs.androidx.tracing.ktx)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.material.icons.extended)
implementation(libs.coil.compose)
implementation(project(":imla"))
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontobfuscate
================================================
FILE: app/src/androidTest/java/dev/serhiiyaremych/imla/ExampleInstrumentedTest.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("dev.serhiiyaremych.imla.test", appContext.packageName)
}
}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/assets/loremipsum.json
================================================
[
{
"id": "cc3a5683-b6bc-4272-8675-a904674f0261",
"userNickname": "@ponder",
"userFullName": "Perry McCray",
"userAvatar": "file:///android_asset/avatars/139.jpg",
"text": "⏸\nSempersenectus dolor tibique euripidis pertinax eget elementum aliquam propriae morbi cubilia proin laudem aptent fugit. Iuslibero neglegentur tota persecuti principes euismod mnesarchum suavitate sanctus quas interdum urbanitas graeco.⛏📹\n",
"images": [],
"likes": 484,
"replyCount": 460,
"messages": 3,
"comments": [
{
"id": "2674562d-edbf-45fd-b43a-4a561bc4309e",
"userNickname": "@delica",
"userFullName": "Doyle Burris",
"userAvatar": "file:///android_asset/avatars/1.jpg",
"text": "adolescens auctor ☁️📚 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974121,
"reply": null,
"poll": null
},
{
"id": "e4d7c77e-d1bb-4467-830e-ab4d5376a47e",
"userNickname": "@instru",
"userFullName": "Sherman Chapman",
"userAvatar": "file:///android_asset/avatars/130.jpg",
"text": "Volumuselementum ridiculus deterruisset sapien eleifend adipiscing dolores iisque fames vituperatoribus placerat perpetua adipiscing vituperatoribus audire simul eam sapien interdum. Eirmodsed habemus latine parturient consectetur elitr montes pertinax inimicus sadipscing eirmod litora aptent. Classsaperet harum at instructior venenatis tale maecenas aliquet aeque invenire cum dicta donec inceptos. Theophrastusfeugiat blandit a tation ante sociis tale consectetuer.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574121,
"reply": null,
"poll": null
},
{
"id": "12043bea-5b2e-4fd2-9a56-e04867db6bed",
"userNickname": "@eam",
"userFullName": "Frederick Pearson",
"userAvatar": "file:///android_asset/avatars/69.jpg",
"text": "Verteremconubia ad tantas melius hac et quisque postea finibus himenaeos consul. Eumeum honestatis.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174121,
"reply": null,
"poll": null
}
],
"createdAt": 1636990374098,
"reply": null,
"poll": null
},
{
"id": "9f3e0565-d12e-4882-9e47-6d9bb441669f",
"userNickname": "@quot",
"userFullName": "Harriet Henderson",
"userAvatar": "file:///android_asset/avatars/85.jpg",
"text": "Fermentumdictumst corrumpit tamquam aeque felis taciti varius feugiat pellentesque appetere veritus nisi singulis sadipscing honestatis neque. Egetsociis egestas fusce saepe urbanitas nominavi delectus similique qualisque patrioque ultricies eirmod. Voluptariaaccumsan an tortor.",
"images": [
"file:///android_asset/post_images/2.jpg",
"file:///android_asset/post_images/4.jpg",
"file:///android_asset/post_images/51.jpg",
"file:///android_asset/post_images/33.jpg",
"file:///android_asset/post_images/19.jpg"
],
"likes": 460,
"replyCount": 9,
"messages": 5,
"comments": [
{
"id": "823b4fe6-e3ce-42ce-9c52-245ee3f35444",
"userNickname": "@in",
"userFullName": "Hector Gaines",
"userAvatar": "file:///android_asset/avatars/56.jpg",
"text": "Posuerecondimentum primis sententiae adversarium fastidii tation per quisque tristique. Natumurbanitas reprimique vis torquent bibendum interesset idque dictum solum. Museget omittam morbi quidam nominavi tacimates tempus dicam mediocritatem veri. Propriaelibris reformidans falli eam reque similique natoque ceteros. Facilispopulo felis accumsan gubergren sociosqu eleifend falli inciderint viverra parturient vestibulum arcu imperdiet mnesarchum faucibus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974121,
"reply": null,
"poll": null
},
{
"id": "6e9ed70c-41fc-45f1-ad0c-83e8aa85f9ae",
"userNickname": "@libris",
"userFullName": "Benjamin Navarro",
"userAvatar": "file:///android_asset/avatars/157.jpg",
"text": "mauris autem a",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174121,
"reply": null,
"poll": null
},
{
"id": "8e6a19b7-c44e-4a12-a7a5-1e35dad75c5d",
"userNickname": "@atqui",
"userFullName": "Mari Mann",
"userAvatar": "file:///android_asset/avatars/151.jpg",
"text": "🥞⏰\nmontes constituto interpretaris luptatum ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574121,
"reply": null,
"poll": null
},
{
"id": "3bfb1e9e-7378-45d2-9738-c571d1fd2a39",
"userNickname": "@magna",
"userFullName": "Darrel Trevino",
"userAvatar": "file:///android_asset/avatars/38.jpg",
"text": "🍯🗃 Repudiarepertinacia nisi vim iusto inani feugiat tamquam. Mediocremnascetur condimentum evertitur falli penatibus harum mollis voluptaria unum unum referrentur. Hassigniferumque reformidans ridiculus noster simul verear efficiantur omnesque partiendo reprehendunt magnis. Promptadictumst porttitor maximus enim minim ultrices his usu deterruisset falli.💰➿\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774122,
"reply": null,
"poll": null
},
{
"id": "15b245db-e6f9-488d-bc46-29de834d264c",
"userNickname": "@advers",
"userFullName": "Trenton Hoover",
"userAvatar": "file:///android_asset/avatars/190.jpg",
"text": "volutpat veniam civibus liber",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974122,
"reply": null,
"poll": null
}
],
"createdAt": 1636986774121,
"reply": null,
"poll": null
},
{
"id": "9ae51c3f-aaf0-4844-9470-01250502364c",
"userNickname": "@falli",
"userFullName": "Jami Cotton",
"userAvatar": "file:///android_asset/avatars/12.jpg",
"text": "🥨🌦 Atquiquaestio sem noster curabitur vero amet dictumst mattis molestie penatibus metus. Tritanimeliore verterem. Atquihabemus praesent lorem parturient. Inaniquam curae nam curae dicant rutrum recteque justo alterum epicuri tempus cursus disputationi ex nihil errem possit movet porta. Molestieleo accusata.🛰🍡🦃 ",
"images": [],
"likes": 720,
"replyCount": 368,
"messages": 7,
"comments": [
{
"id": "a32f638a-d2d1-4839-b45c-a1f7beba6679",
"userNickname": "@ponder",
"userFullName": "Martha Chapman",
"userAvatar": "file:///android_asset/avatars/170.jpg",
"text": "🏺🏗↩️ Fastidiiluptatum suscipit ornare qui. Adversariumarcu putent. Evertiturtale theophrastus autem sit omnesque principes quem erroribus electram. Propriaevim ceteros a gloriatur ornatus eum gubergren aliquet suscipiantur. Dictasimperdiet euripidis torquent molestie delectus pharetra nam dicant repudiandae at consetetur docendi arcu libris salutatus libero.😙🥡 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774122,
"reply": null,
"poll": null
},
{
"id": "1783107c-3ed1-4a4e-801b-b65ffd6b9956",
"userNickname": "@graece",
"userFullName": "Vaughn Langley",
"userAvatar": "file:///android_asset/avatars/119.jpg",
"text": "suas pericula 🌃🔕 mediocritatem nisl veniam 🕓 🐴💻 inceptos \n💉🚘 ↔️🚗 docendi 🚖 habemus conubia mazim 🍄 👇🕙 molestiae \n🤦♂ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774122,
"reply": null,
"poll": null
},
{
"id": "f9dcc7a2-7fec-499d-b690-d0f45b3775dd",
"userNickname": "@region",
"userFullName": "Thomas Simon",
"userAvatar": "file:///android_asset/avatars/112.jpg",
"text": "periculis electram sed ultricies",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974122,
"reply": null,
"poll": null
},
{
"id": "292d7635-d909-4072-b8ab-6d96e082936a",
"userNickname": "@gravid",
"userFullName": "Reynaldo Salas",
"userAvatar": "file:///android_asset/avatars/123.jpg",
"text": "🦒🤑👩🌾 dicant deseruisse iusto\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574122,
"reply": null,
"poll": null
},
{
"id": "914ed688-8ff6-4b7e-b2d9-288e7965cbee",
"userNickname": "@patrio",
"userFullName": "Ahmad Hines",
"userAvatar": "file:///android_asset/avatars/70.jpg",
"text": "Bibendumlatine ipsum accumsan corrumpit gloriatur hendrerit saperet platonem novum quas fugit mentitum aliquet iusto. Volumuseam tation cras reformidans inimicus rutrum magnis. Laudemluptatum venenatis litora tritani quem fugit ante ridiculus invidunt libero corrumpit. Utamurverear debet tota reprimique eirmod quisque brute. Ponderumvocibus quis conclusionemque dis.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374122,
"reply": null,
"poll": null
},
{
"id": "f357b851-33e8-4738-ba05-ce9cd57ab26a",
"userNickname": "@nunc",
"userFullName": "Sheena Elliott",
"userAvatar": "file:///android_asset/avatars/40.jpg",
"text": "urna",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974122,
"reply": null,
"poll": null
},
{
"id": "04dc1a2a-5512-4e4f-9384-367a88e071aa",
"userNickname": "@mazim",
"userFullName": "Rickey Sweeney",
"userAvatar": "file:///android_asset/avatars/58.jpg",
"text": "voluptatibus",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174122,
"reply": null,
"poll": null
}
],
"createdAt": 1636939974122,
"reply": null,
"poll": null
},
{
"id": "16897794-47be-426d-8555-001eab8772bf",
"userNickname": "@pellen",
"userFullName": "Jerry Mathews",
"userAvatar": "file:///android_asset/avatars/142.jpg",
"text": "patrioque definitionem",
"images": [],
"likes": 99,
"replyCount": 142,
"messages": 0,
"comments": [],
"createdAt": 1636939974123,
"reply": {
"id": "e9ad31da-ce0a-46fb-85fb-c9c23a8065e2",
"userNickname": "@porta",
"userFullName": "Jayson Hutchinson",
"userAvatar": "file:///android_asset/avatars/88.jpg",
"text": "Integergraecis vocibus iudicabit maluisset inceptos possit esse nominavi bibendum accumsan voluptaria. Suavitategraece finibus feugait. Gravidadeseruisse mei maximus potenti reprehendunt salutatus finibus laoreet inciderint. Fuissetvulputate porttitor dolorem varius natoque. Facilismediocritatem idque adolescens electram eos eius tibique nobis.",
"images": [],
"likes": 313,
"replyCount": 665,
"messages": 9,
"comments": [
{
"id": "78b23e1d-b4c3-4223-8082-f66427ff8d20",
"userNickname": "@vestib",
"userFullName": "Christina Simon",
"userAvatar": "file:///android_asset/avatars/141.jpg",
"text": "🏮\nVerocursus est ante sit vocent cu docendi epicurei. Doloreappareat fames verear. Scriptalabores autem mus option discere idque intellegat eripuit rhoncus orci habitasse.🥪😦⛱ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174123,
"reply": null,
"poll": null
},
{
"id": "09c39f43-3555-4b81-bb3f-35f3b77da8c8",
"userNickname": "@dapibu",
"userFullName": "Mitchell Burns",
"userAvatar": "file:///android_asset/avatars/45.jpg",
"text": "Salutatusmollis dolores necessitatibus lectus aliquam penatibus a suas dolor repudiare. Assueveritneque posuere duo eirmod hendrerit audire homero errem.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974123,
"reply": null,
"poll": null
},
{
"id": "371c4181-01f2-4cf8-92e1-b3c437dd3449",
"userNickname": "@inani",
"userFullName": "Enid Fisher",
"userAvatar": "file:///android_asset/avatars/150.jpg",
"text": "☃️💽🥧\niudicabit adversarium ullamcorper patrioque evertitur\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974123,
"reply": null,
"poll": null
},
{
"id": "91687fa5-c783-45f4-8330-84aba0e5314c",
"userNickname": "@vehicu",
"userFullName": "Winfred Cherry",
"userAvatar": "file:///android_asset/avatars/171.jpg",
"text": "Contentionesultrices ligula donec legimus duis odio dicant populo aeque fabulas. Nostrumultrices iisque. Dicitfabulas definitionem vestibulum expetenda. Legerevituperata magna nobis tractatos quas auctor error convallis inciderint comprehensam. Indoctumpericula solum molestiae malesuada iisque suscipiantur eleifend gubergren minim hac evertitur accusata ad salutatus invenire cu convallis.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174123,
"reply": null,
"poll": null
},
{
"id": "78bc09ca-e6eb-4b7b-94e3-5249474bda5f",
"userNickname": "@hac",
"userFullName": "Nick Valentine",
"userAvatar": "file:///android_asset/avatars/178.jpg",
"text": "commune pharetra electram urbanitas quot",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974123,
"reply": null,
"poll": null
},
{
"id": "2c252856-0857-4461-9434-e712991a23fc",
"userNickname": "@incept",
"userFullName": "Jeff Dorsey",
"userAvatar": "file:///android_asset/avatars/91.jpg",
"text": "⬇️\nDoctusturpis aliquet morbi antiopam curae contentiones. Unumsagittis tortor urbanitas viderer efficitur tortor nulla an vix brute repudiare falli audire. Ceterosjusto electram lorem singulis omittam noluisse consul habeo lacinia vulputate curae. Dicatiusto augue eirmod eget doming percipit dissentiunt quem. Cursuseuripidis vel phasellus malorum viris vivamus facilis praesent pulvinar rutrum saperet mutat sanctus.🔟🌖🍊\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974123,
"reply": null,
"poll": null
},
{
"id": "1e7c422a-ae5c-45c6-8c91-f7991c8010d0",
"userNickname": "@mucius",
"userFullName": "Alberta Mills",
"userAvatar": "file:///android_asset/avatars/169.jpg",
"text": "🍊\nAmetmagnis movet unum. Homeroiusto diam persequeris utamur aperiri efficitur mediocrem quod animal atqui bibendum felis. Verimediocrem risus ferri. Justopellentesque lorem tristique splendide natum libris cursus accumsan definitiones nonumes tortor venenatis.📓🥌🥐\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974123,
"reply": null,
"poll": null
},
{
"id": "4b662f3e-deba-43f2-98a6-d639898a20e1",
"userNickname": "@amet",
"userFullName": "Frederic Gonzalez",
"userAvatar": "file:///android_asset/avatars/106.jpg",
"text": "habitasse",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974123,
"reply": null,
"poll": null
},
{
"id": "1a8fecfa-1dbb-43f3-a279-f8bf761b0da4",
"userNickname": "@eirmod",
"userFullName": "Charles Johnson",
"userAvatar": "file:///android_asset/avatars/26.jpg",
"text": "Laboresiaculis facilis salutatus graeci cetero assueverit indoctum voluptatibus enim rutrum vituperatoribus veritus a sea saperet dicit hendrerit. Inviduntmaluisset oratio volutpat comprehensam platea pulvinar viverra nascetur adolescens reformidans.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374123,
"reply": null,
"poll": null
}
],
"createdAt": 1636939974123,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "e4557916-05ba-4e67-8958-c988a5e74548",
"userNickname": "@compre",
"userFullName": "Steven Neal",
"userAvatar": "file:///android_asset/avatars/196.jpg",
"text": "tractatos has nihil tota torquent",
"images": [],
"likes": 23,
"replyCount": 36,
"messages": 0,
"comments": [],
"createdAt": 1637011974125,
"reply": {
"id": "7b90b8c0-d096-4b8e-ac44-89c34ebb403b",
"userNickname": "@iudica",
"userFullName": "Milton Farmer",
"userAvatar": "file:///android_asset/avatars/67.jpg",
"text": "noluisse elementum tota nonumes",
"images": [],
"likes": 94,
"replyCount": 89,
"messages": 3,
"comments": [
{
"id": "0f8abba7-f901-4b8c-bc5b-1bca0b0b4075",
"userNickname": "@iisque",
"userFullName": "Ladonna Herring",
"userAvatar": "file:///android_asset/avatars/155.jpg",
"text": "dictas aperiri porta",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774125,
"reply": null,
"poll": null
},
{
"id": "4ee55911-b0a1-4022-8240-4755a8ad1a4c",
"userNickname": "@his",
"userFullName": "Mauricio Hester",
"userAvatar": "file:///android_asset/avatars/156.jpg",
"text": "🥟 honestatis🌎📺🥙\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974125,
"reply": null,
"poll": null
},
{
"id": "bfc47d83-7452-4ac9-affe-e860a44db64a",
"userNickname": "@tempor",
"userFullName": "Elton Wood",
"userAvatar": "file:///android_asset/avatars/47.jpg",
"text": "Percipitdignissim tempor aliquam sociosqu fringilla orci appetere verterem netus. Audireesse graece ferri solet adhuc. Alteranoluisse scelerisque ac vituperata. Vulputatefacilisi penatibus tritani quis suspendisse eloquentiam ludus tristique ridiculus indoctum orci. Nisipharetra eleifend hac. Voluptatumcurae legere aliquip.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774125,
"reply": null,
"poll": null
}
],
"createdAt": 1637011974123,
"reply": null,
"poll": {
"isCompleted": false,
"totalVotes": 1013,
"positions": [
{
"text": "orci dictas",
"voted": 71
},
{
"text": "aliquip",
"voted": 279
},
{
"text": "conubia elementum nobis nostrum",
"voted": 412
},
{
"text": "ancillae adversarium liber feugait aptent",
"voted": 75
},
{
"text": "expetenda quis pro",
"voted": 176
}
]
}
},
"poll": null
},
{
"id": "5739ca94-a595-4d4f-93c0-693fe402052c",
"userNickname": "@quem",
"userFullName": "Maggie Cotton",
"userAvatar": "file:///android_asset/avatars/19.jpg",
"text": "Pertinaxtincidunt nisl. Promptafermentum ornatus atqui mus docendi decore tale facilis mentitum te risus regione sadipscing novum.",
"images": [
"file:///android_asset/post_images/53.jpg",
"file:///android_asset/post_images/31.jpg",
"file:///android_asset/post_images/95.jpg",
"file:///android_asset/post_images/86.jpg"
],
"likes": 62,
"replyCount": 642,
"messages": 1,
"comments": [
{
"id": "58f8d963-5dde-420d-bc96-af2ec6330629",
"userNickname": "@suas",
"userFullName": "Wendell Luna",
"userAvatar": "file:///android_asset/avatars/61.jpg",
"text": "❇️🐼\nMediocremiriure tempus corrumpit. Dissentiuntpharetra dapibus affert accumsan moderatius. Aperirilegere molestie molestiae convenire te propriae felis mei vestibulum deseruisse volutpat. Persiusreprehendunt vocibus pharetra habitasse fusce est prodesset in ei.\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974125,
"reply": null,
"poll": null
}
],
"createdAt": 1637004774125,
"reply": null,
"poll": null
},
{
"id": "4aad95e1-e5d6-465f-9922-2ff11ecd58f9",
"userNickname": "@posuer",
"userFullName": "Freda Rosa",
"userAvatar": "file:///android_asset/avatars/65.jpg",
"text": "🥨📭 dolorem \n👩👩👧 luctus quot \n🛣 disputationi solum 🐌 📱🐟 inceptos \n😎🚇 👕🤣 feugait 🚨👒 🥤🛵 posse \n👨💻 putent mandamus \n🐒 ⬅️👭 gravida \n🌍💙 ",
"images": [
"file:///android_asset/post_images/21.jpg",
"file:///android_asset/post_images/65.jpg",
"file:///android_asset/post_images/73.jpg"
],
"likes": 321,
"replyCount": 563,
"messages": 4,
"comments": [
{
"id": "27571184-2b66-4ee4-94ee-ab01f594f1a9",
"userNickname": "@dolore",
"userFullName": "Annette Savage",
"userAvatar": "file:///android_asset/avatars/181.jpg",
"text": "🍒 deseruisse🏣 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974125,
"reply": null,
"poll": null
},
{
"id": "e036703a-2d47-43f8-b488-70da2d313c04",
"userNickname": "@nobis",
"userFullName": "Emmanuel Macias",
"userAvatar": "file:///android_asset/avatars/102.jpg",
"text": "Atefficiantur auctor falli animal id curae velit inceptos falli facilis litora pulvinar evertitur mi condimentum. Sumovidisse persecuti rutrum salutatus vix aliquet dicunt pulvinar quidam delicata senectus idque fabellas suscipit feugait tacimates tritani leo. Imperdietlaudem montes sapientem. Convallisrhoncus meliore minim appareat sodales eruditi lorem lectus senserit neque pertinacia velit lorem ac vituperatoribus libris assueverit periculis potenti. Epicureimaximus natoque lacus nisi ornatus interdum tantas voluptatibus mi solet facilis singulis dico interpretaris orci scelerisque. Sociisarcu idque dico mediocrem pellentesque nisi curabitur magnis nonumes adolescens graeco gubergren graecis litora.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374125,
"reply": null,
"poll": null
},
{
"id": "ce421f84-0dfc-4b08-a2b8-28e59ea0d1ed",
"userNickname": "@mollis",
"userFullName": "Leona Fleming",
"userAvatar": "file:///android_asset/avatars/80.jpg",
"text": "sociosqu per duo",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374126,
"reply": null,
"poll": null
},
{
"id": "d7050a9b-afea-45b5-ad89-bc65332dd8b5",
"userNickname": "@indoct",
"userFullName": "Lakisha Wolfe",
"userAvatar": "file:///android_asset/avatars/134.jpg",
"text": "posidonium ancillae offendit agam diam",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174126,
"reply": null,
"poll": null
}
],
"createdAt": 1636993974125,
"reply": null,
"poll": null
},
{
"id": "9e6f3399-c817-4304-84f8-c91f34a61021",
"userNickname": "@tracta",
"userFullName": "Wendell Odom",
"userAvatar": "file:///android_asset/avatars/129.jpg",
"text": "Eiusverterem finibus qui pri suavitate. Ferritincidunt sadipscing et minim risus ea tempus corrumpit mattis maecenas simul phasellus tacimates sit. Possecum tation partiendo justo invenire vulputate rhoncus. Honestatiscubilia feugiat graecis vidisse putent latine omittantur gravida fermentum alia pulvinar gloriatur tristique inimicus aliquid sonet usu neglegentur volutpat.",
"images": [],
"likes": 277,
"replyCount": 424,
"messages": 7,
"comments": [
{
"id": "37ba58d7-5b82-4681-b77a-2ae98be6a492",
"userNickname": "@porro",
"userFullName": "Reynaldo Zimmerman",
"userAvatar": "file:///android_asset/avatars/20.jpg",
"text": "🃏🌮 movet \n✝️🗄 📝👩🏫 civibus \n🤑 ⛺️🐐 solet \n®️ 🍢🌏 sapien ⛓📘 proin solet nihil \n🎠 penatibus putent postulant \n🚠🍬 nihil euismod facilisi ⚓️ alienum mauris \n🌾 quis pri 🈯️👂 falli adipiscing 🥠 repudiandae equidem 👗💵 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174126,
"reply": null,
"poll": null
},
{
"id": "795c7d1d-9430-475d-8226-c0792266dd4e",
"userNickname": "@lectus",
"userFullName": "Ericka Wood",
"userAvatar": "file:///android_asset/avatars/182.jpg",
"text": "postea ancillae",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374126,
"reply": null,
"poll": null
},
{
"id": "f701bf7b-5660-48be-95e8-ce9d955d7e8c",
"userNickname": "@tritan",
"userFullName": "Raphael Raymond",
"userAvatar": "file:///android_asset/avatars/43.jpg",
"text": "error nisl iuvaret condimentum",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374126,
"reply": null,
"poll": null
},
{
"id": "0a75c41a-17df-4052-97c1-b20e2e52bd87",
"userNickname": "@facili",
"userFullName": "Eugenio Cervantes",
"userAvatar": "file:///android_asset/avatars/161.jpg",
"text": "🎊\nvim🌾🍩6️⃣\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174126,
"reply": null,
"poll": null
},
{
"id": "82d7da6c-2b59-4b0c-b56d-ec4eba4954bb",
"userNickname": "@ornatu",
"userFullName": "Letha Gay",
"userAvatar": "file:///android_asset/avatars/189.jpg",
"text": "📧🚖 omnesque 📇 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974126,
"reply": null,
"poll": null
},
{
"id": "7c5e52b5-c599-4268-a648-71baed2ff5d9",
"userNickname": "@luptat",
"userFullName": "Seth David",
"userAvatar": "file:///android_asset/avatars/165.jpg",
"text": "Dicamfusce condimentum ubique movet theophrastus pulvinar mattis elaboraret melius voluptaria dico consectetuer. Metusfermentum suavitate ancillae pharetra theophrastus vix congue eloquentiam reprehendunt dictumst suscipit. Duisfacilisis mandamus salutatus malesuada maximus mediocrem harum ancillae habitasse expetenda feugait inceptos invenire eos legere quaeque audire etiam moderatius. Communetractatos inani mea no maiestatis putent errem aeque dictas metus iisque scripserit duo. Nibhgraeci eirmod persequeris qui consectetur disputationi vel. Esthinc no an arcu vestibulum saperet vehicula praesent curabitur inciderint adolescens sale nobis partiendo viris accumsan noluisse diam gloriatur.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374126,
"reply": null,
"poll": null
},
{
"id": "d41b3dde-2db2-40ec-ade5-098c8352e93c",
"userNickname": "@quot",
"userFullName": "Kennith Gamble",
"userAvatar": "file:///android_asset/avatars/104.jpg",
"text": "quot blandit patrioque 🏕 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974126,
"reply": null,
"poll": null
}
],
"createdAt": 1636939974126,
"reply": null,
"poll": null
},
{
"id": "3f3c3717-42ce-478a-9021-191c37dc1424",
"userNickname": "@donec",
"userFullName": "Etta Nicholson",
"userAvatar": "file:///android_asset/avatars/146.jpg",
"text": "Molestiaepraesent electram no hinc instructior posse dictumst volumus scripserit morbi dicunt errem eget scelerisque sollicitudin. Utinamornatus suscipit nobis eleifend signiferumque suspendisse suas sit dicunt graece aliquet feugiat duo expetendis vocent possim nam. Ipsumet sit pertinacia altera ultricies iusto comprehensam option aliquet vulputate duis nihil qui singulis ante. Aperiripro quaerendum suscipiantur putent id maximus has movet orci voluptaria errem quisque donec mel ultrices expetenda tempus tation laoreet. Elitpersequeris tortor utamur sodales conceptam constituam suas dapibus hendrerit scelerisque mei sociis dissentiunt primis voluptatibus aenean te ne accommodare. Menandriatqui consectetuer risus sententiae nam adolescens eruditi falli inimicus pellentesque regione agam.",
"images": [
"file:///android_asset/post_images/54.jpg",
"file:///android_asset/post_images/38.jpg"
],
"likes": 804,
"replyCount": 294,
"messages": 1,
"comments": [
{
"id": "8839a758-f063-4af8-ae19-5a9fbabfd9e5",
"userNickname": "@curae",
"userFullName": "Irene Pratt",
"userAvatar": "file:///android_asset/avatars/27.jpg",
"text": "Vitaehabeo partiendo nihil viderer. Consectetuerdis ac aliquip euripidis equidem. Populorepudiare conceptam pertinax decore saperet. Oporteatoption mollis omittantur sem luptatum graecis alia utamur possit tractatos potenti porttitor utamur hinc efficiantur equidem. Harumdolorum homero lacinia quot adhuc delectus tempor senectus dicant wisi dolor inimicus propriae cursus meliore. Senserithomero torquent labores molestiae sonet eum tristique libris gloriatur impetus detraxit sea aliquam cu.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174127,
"reply": null,
"poll": null
}
],
"createdAt": 1637001174126,
"reply": null,
"poll": null
},
{
"id": "20afd082-15b0-4b39-93d0-736e705dbbe5",
"userNickname": "@accums",
"userFullName": "Juliette Campos",
"userAvatar": "file:///android_asset/avatars/197.jpg",
"text": "🍼🤤 efficiantur \n🌊 erat conclusionemque \n🦊 💿👯 libero \n🌎🌁 👹♣️ efficiantur 🆎 🚲📇 faucibus 😟🥦 sea pri 🦑 cetero hac deserunt ⚱️ expetendis expetenda 💓🍢 malesuada condimentum 🍵🕵 ",
"images": [],
"likes": 425,
"replyCount": 314,
"messages": 3,
"comments": [
{
"id": "7a9bc732-7cc7-497b-b0b5-a291b5343c03",
"userNickname": "@iisque",
"userFullName": "Elena Roth",
"userAvatar": "file:///android_asset/avatars/94.jpg",
"text": "principes doctus porta magna viderer",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374127,
"reply": null,
"poll": null
},
{
"id": "f4d44856-6b4b-4705-96df-f9bfb59fecc0",
"userNickname": "@alienu",
"userFullName": "Scott Silva",
"userAvatar": "file:///android_asset/avatars/137.jpg",
"text": "imperdiet nostra",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974127,
"reply": null,
"poll": null
},
{
"id": "2fd05e40-e472-4c7a-b592-5967bd39b388",
"userNickname": "@vocent",
"userFullName": "Sofia Lowery",
"userAvatar": "file:///android_asset/avatars/101.jpg",
"text": "🌃🍵 veritus ludus🔭🆑\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174127,
"reply": null,
"poll": null
}
],
"createdAt": 1636986774127,
"reply": null,
"poll": null
},
{
"id": "7d5e02a0-0763-416f-9aad-bdffc3a292d4",
"userNickname": "@errem",
"userFullName": "Jim Koch",
"userAvatar": "file:///android_asset/avatars/135.jpg",
"text": "🥛👨✈ utamur ",
"images": [],
"likes": 83,
"replyCount": 90,
"messages": 0,
"comments": [],
"createdAt": 1637001174127,
"reply": {
"id": "7744a4f1-74e7-4533-988f-f21c21b328f1",
"userNickname": "@habita",
"userFullName": "Morton Bender",
"userAvatar": "file:///android_asset/avatars/62.jpg",
"text": "Principesrhoncus recteque saepe quaestio dapibus sadipscing delectus reprimique ultricies legimus sumo contentiones suspendisse ex meliore mediocritatem pri veniam. Maecenasoffendit ut hac ne suas eos nisi class volumus egestas cetero definitionem integer eirmod. Tacitipretium pretium no melius quod integer decore quaerendum nulla malorum mollis quas eu porttitor animal phasellus.",
"images": [],
"likes": 225,
"replyCount": 48,
"messages": 5,
"comments": [
{
"id": "948b1eeb-7680-4f5f-a58e-b7d885a2fd7e",
"userNickname": "@quo",
"userFullName": "Julianne Alexander",
"userAvatar": "file:///android_asset/avatars/6.jpg",
"text": "Doloresquod sumo eripuit no fermentum altera dapibus lorem necessitatibus scripserit putent posidonium mucius qui hinc consul debet fabellas. Ametutamur vulputate molestiae suspendisse fabellas ferri auctor voluptatibus viris nonumy tota elit. Librissumo homero. Alialaudem nobis mnesarchum. Nonumyexpetendis scelerisque quidam mauris eos agam regione habitasse dicat alterum sagittis sanctus. Consetetursapientem dolor nam option vestibulum.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574127,
"reply": null,
"poll": null
},
{
"id": "10566497-6065-469c-baf9-8af20b1cbc4b",
"userNickname": "@potent",
"userFullName": "Vance Aguilar",
"userAvatar": "file:///android_asset/avatars/111.jpg",
"text": "molestie",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374127,
"reply": null,
"poll": null
},
{
"id": "afddeddd-2280-43ac-b1ac-192a2a200c23",
"userNickname": "@tincid",
"userFullName": "Graham Gay",
"userAvatar": "file:///android_asset/avatars/89.jpg",
"text": "Numquamdiam cetero congue tation vim conclusionemque conclusionemque. Dictasit veri necessitatibus mei montes tantas qui facilis tation quod. Usumelius faucibus contentiones repudiare tortor per ex pro populo quod quaestio legere viderer tincidunt non pretium libero agam.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574127,
"reply": null,
"poll": null
},
{
"id": "cc6ff0fd-d4b1-4764-9f18-6fe81f8e8812",
"userNickname": "@concep",
"userFullName": "Wilbur Murray",
"userAvatar": "file:///android_asset/avatars/34.jpg",
"text": "Possitdiscere luctus veniam platea. Donecconceptam persecuti quo vidisse ridens a dolor hac felis viderer pri error invenire. Communediscere adipiscing conclusionemque. Veroceteros alienum cursus class etiam deterruisset a animal nostrum per dolore porro ut. Pulvinarqualisque mnesarchum pharetra curae finibus dictum convallis porta euripidis ornare doming cu volutpat vel sociis vitae.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574127,
"reply": null,
"poll": null
},
{
"id": "a7053c74-239d-4458-94ca-3920d0f38a10",
"userNickname": "@ipsum",
"userFullName": "Tabatha Roman",
"userAvatar": "file:///android_asset/avatars/120.jpg",
"text": "commune vivamus eos \n💆 habemus appetere 💞 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574127,
"reply": null,
"poll": null
}
],
"createdAt": 1636993974127,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "7a9a593f-5554-44b0-b8bf-f1d7d5d92e75",
"userNickname": "@est",
"userFullName": "Jeremy Hodges",
"userAvatar": "file:///android_asset/avatars/191.jpg",
"text": "eleifend affert",
"images": [],
"likes": 65,
"replyCount": 114,
"messages": 0,
"comments": [],
"createdAt": 1636968774127,
"reply": {
"id": "68171fb6-9c39-4a08-92be-2936012f67af",
"userNickname": "@inimic",
"userFullName": "Nelson Pena",
"userAvatar": "file:///android_asset/avatars/7.jpg",
"text": "condimentum definitionem suscipit 🐯📲 simul te singulis \n🐞 nulla himenaeos pulvinar \n⛩ 🍒🛶 inceptos 🔐👨👩👧👧 turpis decore epicuri \n⛲️ ante parturient aeque 🌠🐍 📞🤑 condimentum 💲 ",
"images": [
"file:///android_asset/post_images/30.jpg",
"file:///android_asset/post_images/10.jpg",
"file:///android_asset/post_images/72.jpg",
"file:///android_asset/post_images/28.jpg"
],
"likes": 833,
"replyCount": 56,
"messages": 2,
"comments": [
{
"id": "b5bb832a-2dd4-4c71-8270-ee7742ebcdb2",
"userNickname": "@patrio",
"userFullName": "Bobbie Snider",
"userAvatar": "file:///android_asset/avatars/13.jpg",
"text": "Tortorfabellas commune ubique adipisci moderatius nibh dolore facilis nisi inciderint expetendis pertinacia purus phasellus. Extacimates facilis propriae.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374128,
"reply": null,
"poll": null
},
{
"id": "dc14136f-c011-44c4-8eb2-42406872f8ec",
"userNickname": "@volupt",
"userFullName": "Terrance Wilcox",
"userAvatar": "file:///android_asset/avatars/163.jpg",
"text": "⛪️💦 Eloquentiamurbanitas ponderum dolore iaculis. Vocentridens penatibus orci dicta reprimique singulis luptatum senectus epicurei movet impetus invidunt egestas mucius solet intellegat himenaeos graeci vocibus. Maluissetdapibus percipit epicuri. Pretiumquem theophrastus repudiandae finibus homero veniam mel malorum sapientem singulis qualisque solet aptent vituperatoribus et alterum petentium eam. Dictasei movet natoque principes repudiandae. ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374128,
"reply": null,
"poll": null
}
],
"createdAt": 1636954374127,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "6bcf2fe3-82b6-4bda-9aa7-b68fb64fd6dd",
"userNickname": "@pellen",
"userFullName": "Cathryn Hardy",
"userAvatar": "file:///android_asset/avatars/124.jpg",
"text": "mediocrem",
"images": [],
"likes": 29,
"replyCount": 144,
"messages": 0,
"comments": [],
"createdAt": 1636993974128,
"reply": {
"id": "74697a81-47e1-4bb0-bb17-66781d378261",
"userNickname": "@senten",
"userFullName": "Carly Berger",
"userAvatar": "file:///android_asset/avatars/17.jpg",
"text": "Veniamdelectus posse utinam gubergren nulla erroribus sociis dictas ius affert animal his inceptos detraxit commodo delectus mutat. Eruditiepicuri vidisse scelerisque quaeque ornatus mei consetetur bibendum nonumes has. Hendreritomittantur netus in eum suscipit eros quod vivendo cras taciti audire tacimates class epicurei quidam velit justo fuisset nobis. Loremnatum sociosqu detraxit wisi nisl corrumpit est mea verterem sem.",
"images": [
"file:///android_asset/post_images/34.jpg",
"file:///android_asset/post_images/37.jpg",
"file:///android_asset/post_images/16.jpg"
],
"likes": 103,
"replyCount": 553,
"messages": 3,
"comments": [
{
"id": "dc953522-a71f-43e1-acc0-3fcec1a5226f",
"userNickname": "@option",
"userFullName": "Sheryl Owen",
"userAvatar": "file:///android_asset/avatars/147.jpg",
"text": "nonumy fusce commune",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974128,
"reply": null,
"poll": null
},
{
"id": "5187b2b6-97c1-49f8-bc9f-9e5507ddca97",
"userNickname": "@melius",
"userFullName": "Deanna Horn",
"userAvatar": "file:///android_asset/avatars/158.jpg",
"text": "Mediocremmorbi graeci fabulas elementum electram civibus iuvaret suspendisse varius taciti accusata sollicitudin. Vidissepertinax iriure sale. Esseefficitur viverra natoque mollis ei nibh brute nulla urna odio oratio honestatis nulla euismod disputationi tristique adipiscing esse dolor. Parturientsimilique urna. Sollicitudindelicata consetetur arcu vocibus mazim repudiandae veritus torquent pericula ornatus egestas eum quaerendum nostrum imperdiet utroque postea vocent viris. Porttitorlaoreet in has eirmod mediocritatem dolorum velit tempor dapibus et facilisis accusata blandit suavitate graece verterem.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774128,
"reply": null,
"poll": null
},
{
"id": "71b89ed9-c1a7-4c50-b3a3-702ed929d491",
"userNickname": "@petent",
"userFullName": "Stevie Weber",
"userAvatar": "file:///android_asset/avatars/63.jpg",
"text": "🐛⭐️ gubergren sanctus💨🚰🚇\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974128,
"reply": null,
"poll": null
}
],
"createdAt": 1636979574128,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "0b88a0c7-be2a-47bb-b8fb-e7dcd4dfe57a",
"userNickname": "@doming",
"userFullName": "Cara Witt",
"userAvatar": "file:///android_asset/avatars/16.jpg",
"text": "mazim dictumst persecuti 🎉 utinam euripidis 🏩🏦 🥥🍸 aliquip 👫🏰 magna senserit tortor 👨👩👧 🐙✈️ lectus \n👎⛪️ 🐡🍾 vocent \n🚬 🥤😡 novum 🚐 🏗📀 ei 🔧⛺️ ⚫️🌏 fabellas 🚬 🚒📲 praesent \n🖲 ",
"images": [
"file:///android_asset/post_images/71.jpg",
"file:///android_asset/post_images/62.jpg",
"file:///android_asset/post_images/27.jpg"
],
"likes": 494,
"replyCount": 427,
"messages": 5,
"comments": [
{
"id": "1120008c-e5be-46cd-b2e3-8d473fc9875f",
"userNickname": "@effici",
"userFullName": "Rogelio Stokes",
"userAvatar": "file:///android_asset/avatars/187.jpg",
"text": "Similiqueadipiscing quis at singulis id ipsum saepe diam pro interesset mollis vidisse posuere harum regione cursus. Ubiquegraeci primis eget liber.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974129,
"reply": null,
"poll": null
},
{
"id": "24a99d6d-d49b-4b79-9efb-8dfe5df4221e",
"userNickname": "@impetu",
"userFullName": "Myles Barnett",
"userAvatar": "file:///android_asset/avatars/11.jpg",
"text": "🖕🛃📁\nparturient magnis iusto hac ea🐯 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174129,
"reply": null,
"poll": null
},
{
"id": "7fe159d3-194e-4ed3-acd0-82ce5dc9fb37",
"userNickname": "@partur",
"userFullName": "Barbra Griffith",
"userAvatar": "file:///android_asset/avatars/131.jpg",
"text": "Torquentinstructior metus repudiare pertinacia gravida elaboraret leo. Accumsanpotenti fuisset doming ridiculus fuisset conubia donec mel tation meliore noster pharetra tamquam omittam malorum dicta. Veriutinam adhuc docendi iuvaret fugit rhoncus laoreet ceteros.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174129,
"reply": null,
"poll": null
},
{
"id": "b33009bb-a39e-44c8-ab58-ff6d35f501fc",
"userNickname": "@nisi",
"userFullName": "Felipe Nolan",
"userAvatar": "file:///android_asset/avatars/48.jpg",
"text": "mi mattis mauris ⛓ 🕸🚀 veniam 🍵 est elaboraret 🌤🥛 🌽🕹 brute \n🌳🏛 ❄️🛅 eget 🍾 causae metus \n👩🚀⏬ doctus consul ridens 🅾️ iaculis phasellus \n↔️ 🐴😗 dicant 🥜👫 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974129,
"reply": null,
"poll": null
},
{
"id": "b6eb5d87-ebf1-44a3-9eec-9a8a553b2f8a",
"userNickname": "@defini",
"userFullName": "Sanford Kramer",
"userAvatar": "file:///android_asset/avatars/128.jpg",
"text": "Nostrainterpretaris purus sed. Vimhabemus interpretaris eget reformidans consequat numquam mandamus tota doming felis sollicitudin utroque diam regione ut senserit nihil offendit. Temporelementum morbi percipit deserunt facilisis maximus solum torquent. Ceterosper morbi porttitor sumo delectus imperdiet dictum.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774129,
"reply": null,
"poll": null
}
],
"createdAt": 1636975974128,
"reply": null,
"poll": null
},
{
"id": "500cee11-2bd5-42f6-9565-d72dee864a90",
"userNickname": "@unum",
"userFullName": "Rudy Strickland",
"userAvatar": "file:///android_asset/avatars/186.jpg",
"text": "vis primis dissentiunt",
"images": [],
"likes": 53,
"replyCount": 97,
"messages": 0,
"comments": [],
"createdAt": 1636975974129,
"reply": {
"id": "958ecd1c-e8cc-4a6e-84c9-6fdaf0a263c7",
"userNickname": "@idque",
"userFullName": "Lorie Thornton",
"userAvatar": "file:///android_asset/avatars/2.jpg",
"text": "eu iriure",
"images": [],
"likes": 156,
"replyCount": 280,
"messages": 4,
"comments": [
{
"id": "3e2330a7-1998-444a-9d3e-a70e7f83bcda",
"userNickname": "@videre",
"userFullName": "Sheryl Weiss",
"userAvatar": "file:///android_asset/avatars/31.jpg",
"text": "amet contentiones possit necessitatibus",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374129,
"reply": null,
"poll": null
},
{
"id": "c3562db9-9385-4951-bae1-dbd97bef7e93",
"userNickname": "@ac",
"userFullName": "Trevor Wilson",
"userAvatar": "file:///android_asset/avatars/82.jpg",
"text": "🍙🙍🌛 pharetra periculis🏬🎛 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174129,
"reply": null,
"poll": null
},
{
"id": "fc9f5574-7987-4d6b-b65a-45cc925e472b",
"userNickname": "@ludus",
"userFullName": "Lacy Sears",
"userAvatar": "file:///android_asset/avatars/136.jpg",
"text": "aliquam urbanitas volutpat sapientem",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174129,
"reply": null,
"poll": null
},
{
"id": "6865f4eb-ccf1-4bac-9c06-287f981eeea8",
"userNickname": "@ne",
"userFullName": "Kimberly Delgado",
"userAvatar": "file:///android_asset/avatars/18.jpg",
"text": "🍽🏭 nostrum 🕵🏞 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174129,
"reply": null,
"poll": null
}
],
"createdAt": 1636972374129,
"reply": null,
"poll": {
"isCompleted": false,
"totalVotes": 944,
"positions": [
{
"text": "mi fringilla autem saperet tritani",
"voted": 360
},
{
"text": "sententiae ultrices cum dui",
"voted": 141
},
{
"text": "hac congue iaculis sale aliquid",
"voted": 443
}
]
}
},
"poll": null
},
{
"id": "cf01eb40-1782-4e6c-932d-70dbd1378951",
"userNickname": "@theoph",
"userFullName": "Dorthy Luna",
"userAvatar": "file:///android_asset/avatars/153.jpg",
"text": "Facilisipetentium sale pharetra consetetur sed. Instructiorinterpretaris quisque repudiandae.",
"images": [
"file:///android_asset/post_images/6.jpg",
"file:///android_asset/post_images/81.jpg"
],
"likes": 551,
"replyCount": 5,
"messages": 3,
"comments": [
{
"id": "61ce5dcf-932f-4e6a-a44f-8622b5a0864a",
"userNickname": "@felis",
"userFullName": "Genaro Hardin",
"userAvatar": "file:///android_asset/avatars/177.jpg",
"text": "vocent facilis",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374129,
"reply": null,
"poll": null
},
{
"id": "feb8cf42-ba17-4dd1-8941-98d0c213be76",
"userNickname": "@repudi",
"userFullName": "Deann David",
"userAvatar": "file:///android_asset/avatars/109.jpg",
"text": "🔁🚸 sumo \n🚏 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174129,
"reply": null,
"poll": null
},
{
"id": "6662a1f1-f483-4bd1-b25f-660420844622",
"userNickname": "@sit",
"userFullName": "Alfredo Bradshaw",
"userAvatar": "file:///android_asset/avatars/41.jpg",
"text": "🔋⁉️ molestie 🉑⏮ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174129,
"reply": null,
"poll": null
}
],
"createdAt": 1636990374129,
"reply": null,
"poll": null
},
{
"id": "11117103-7bca-4d82-a224-e8e2d3d62a5e",
"userNickname": "@pretiu",
"userFullName": "Keri Gray",
"userAvatar": "file:///android_asset/avatars/138.jpg",
"text": "torquent",
"images": [],
"likes": 11,
"replyCount": 101,
"messages": 0,
"comments": [],
"createdAt": 1636947174129,
"reply": {
"id": "81a3bfcd-9f4d-46ea-9170-b8312f850574",
"userNickname": "@volupt",
"userFullName": "Sophia Walter",
"userAvatar": "file:///android_asset/avatars/172.jpg",
"text": "fabellas inciderint postea ac",
"images": [],
"likes": 259,
"replyCount": 249,
"messages": 0,
"comments": [],
"createdAt": 1636932774129,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 740,
"positions": [
{
"text": "eos",
"voted": 8
},
{
"text": "equidem alterum electram iudicabit",
"voted": 245
},
{
"text": "hac atomorum",
"voted": 459
},
{
"text": "maiorum quod aliquid habitasse partiendo",
"voted": 28
}
]
}
},
"poll": null
},
{
"id": "87c62d13-257b-4a09-b21c-d4cdc05bcdb3",
"userNickname": "@no",
"userFullName": "Shawna Maynard",
"userAvatar": "file:///android_asset/avatars/60.jpg",
"text": "✅🌎☄\nVispri vivendo. Muciusvocent tale ceteros ullamcorper audire sociosqu elit pericula tota labores dicit quot utinam similique indoctum inciderint tincidunt. Oporteatdefinitiones platonem inimicus homero pertinacia honestatis mei audire consectetuer augue labores detracto vituperata sale mollis patrioque voluptaria tale vitae.®️👘🌱\n",
"images": [],
"likes": 40,
"replyCount": 380,
"messages": 5,
"comments": [
{
"id": "91453676-290c-4c5d-8922-0cd3fb28ffd2",
"userNickname": "@movet",
"userFullName": "Max Ford",
"userAvatar": "file:///android_asset/avatars/144.jpg",
"text": "Curabiturcum constituto error maiorum fringilla novum omnesque purus scripta molestiae molestie rhoncus his lobortis lobortis adolescens. Propriaefugit convallis est salutatus. Eammolestie repudiare elementum percipit habitant consectetuer sententiae rhoncus possim. Petentiumdecore nulla posuere nascetur erroribus fabulas adipiscing scripta detraxit posse dolore dolores penatibus conclusionemque. Periculispropriae assueverit theophrastus dapibus vehicula commune blandit placerat novum offendit.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174130,
"reply": null,
"poll": null
},
{
"id": "cb7441b7-5e93-40c1-a4e3-cdaad3bea213",
"userNickname": "@potent",
"userFullName": "Terrence Chase",
"userAvatar": "file:///android_asset/avatars/118.jpg",
"text": "quisque quod noluisse",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174130,
"reply": null,
"poll": null
},
{
"id": "349e544e-bc47-4fac-93c5-8e08004262d4",
"userNickname": "@quidam",
"userFullName": "Amber Tillman",
"userAvatar": "file:///android_asset/avatars/148.jpg",
"text": "Voluptariafacilisis viris iriure ea utamur antiopam curabitur. Graecivero reprehendunt solet ridens accumsan dui consul ei sed porro habeo omnesque ea viris. Repudiareprompta tractatos sale lobortis eius dui pellentesque laudem voluptatibus orci conceptam prompta quas utroque prodesset.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374130,
"reply": null,
"poll": null
},
{
"id": "cfc9f125-6fa2-422c-bc01-fea4827758e1",
"userNickname": "@deleni",
"userFullName": "Chris Morrow",
"userAvatar": "file:///android_asset/avatars/24.jpg",
"text": "🐶 novum😃🚽\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974130,
"reply": null,
"poll": null
},
{
"id": "cd2176db-5b87-4c70-a127-bbc687bfbc9d",
"userNickname": "@mutat",
"userFullName": "Sidney Ellis",
"userAvatar": "file:///android_asset/avatars/97.jpg",
"text": "tincidunt facilisis ridens dicam",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174130,
"reply": null,
"poll": null
}
],
"createdAt": 1637011974129,
"reply": null,
"poll": null
},
{
"id": "1efaeebb-c519-4bbf-ac26-2f993f0b6b77",
"userNickname": "@eripui",
"userFullName": "Wendell McDaniel",
"userAvatar": "file:///android_asset/avatars/37.jpg",
"text": "dolores fabulas \n🐑 ",
"images": [],
"likes": 12,
"replyCount": 2,
"messages": 0,
"comments": [],
"createdAt": 1636968774130,
"reply": {
"id": "0fb6238a-8966-4619-89ea-1ade1ec44bff",
"userNickname": "@sanctu",
"userFullName": "Kirsten Wyatt",
"userAvatar": "file:///android_asset/avatars/28.jpg",
"text": "Vocentsodales gubergren iudicabit graecis. Priluctus suavitate viderer elementum laudem appetere dicam. Maecenasmei persius vivendo viris pharetra mei viris pro mus velit ferri donec elit primis praesent movet quem. Wisitortor latine error.",
"images": [],
"likes": 522,
"replyCount": 549,
"messages": 3,
"comments": [
{
"id": "890a989c-6df0-4384-b4d5-10737194299a",
"userNickname": "@ocurre",
"userFullName": "Eddie Leblanc",
"userAvatar": "file:///android_asset/avatars/98.jpg",
"text": "🐁🍃 congue \n🖼🥩 ✈️🌋 velit 🐁 🕕🈺 singulis ⬇️🍕 🔱🍞 suas 🍞 recteque meliore \n⚒ convenire fringilla 💙🔩 🛌👯 ultricies \n🚑✉️ ♋️🐁 aptent 🙊🧦 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174130,
"reply": null,
"poll": null
},
{
"id": "7810f05c-ff0c-448d-ae75-e1468547b420",
"userNickname": "@mentit",
"userFullName": "Bruno Gamble",
"userAvatar": "file:///android_asset/avatars/152.jpg",
"text": "contentiones blandit fugit 🍅 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774130,
"reply": null,
"poll": null
},
{
"id": "36f57c6f-1db9-49f8-b9d8-4018453bb648",
"userNickname": "@theoph",
"userFullName": "Rogelio Blackburn",
"userAvatar": "file:///android_asset/avatars/4.jpg",
"text": "☘️✌️ Petentiumcursus aeque mattis eripuit dico maiestatis aliquam noluisse. Potentiintellegat utroque adipisci ne volumus suspendisse corrumpit porro option arcu liber dolorum odio vel hendrerit varius. Quidamtortor viverra mutat vim adolescens melius augue arcu perpetua nonumy eget mi interesset simul eius placerat suas magnis. Mentitumaperiri lectus cu invenire.🌏🏝 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774130,
"reply": null,
"poll": null
}
],
"createdAt": 1636968774130,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "17b85247-bdfd-45c8-b008-f32de9cea6e3",
"userNickname": "@videre",
"userFullName": "Lee Fulton",
"userAvatar": "file:///android_asset/avatars/114.jpg",
"text": "👩❤️👩\nceteros mus platea epicuri\n",
"images": [],
"likes": 71,
"replyCount": 182,
"messages": 0,
"comments": [],
"createdAt": 1636972374130,
"reply": {
"id": "e875f83c-4426-42ff-adc7-70356b2734aa",
"userNickname": "@amet",
"userFullName": "Lesa Mueller",
"userAvatar": "file:///android_asset/avatars/167.jpg",
"text": "Oratiocorrumpit mel dictas praesent diam cubilia instructior eripuit vim. Decorenulla ea atqui mus possim an justo fusce vivendo scripta possim splendide tation homero vituperata eleifend condimentum. Eiorci a ullamcorper bibendum massa. Posuereimpetus nec a vix est eruditi aeque indoctum reque feugait unum signiferumque constituam impetus urbanitas.",
"images": [],
"likes": 800,
"replyCount": 96,
"messages": 2,
"comments": [
{
"id": "abf3631b-9e48-43c2-9d79-88730b1090a5",
"userNickname": "@pellen",
"userFullName": "Jana Chambers",
"userAvatar": "file:///android_asset/avatars/71.jpg",
"text": "Mnesarchumadipisci gloriatur propriae tantas menandri. Sociosquiaculis faucibus conubia euripidis duis suspendisse conceptam mediocritatem sapientem discere luctus faucibus unum te molestie postea explicari nonumy nisi. Librisrepudiare assueverit mattis propriae an per suavitate dis cras consul vero affert rhoncus egestas explicari eripuit. Liberosolet nihil expetendis salutatus tellus melius magnis simul quis. Eumaperiri a appareat maximus imperdiet graecis utinam lobortis mei quot iisque curae euismod ne vero populo convallis efficiantur potenti. Ludusgravida ea mutat augue aliquip interdum sadipscing.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774130,
"reply": null,
"poll": null
},
{
"id": "dd4aa38e-5f42-4526-a0d3-dfe70794a0c3",
"userNickname": "@nulla",
"userFullName": "Norman Horton",
"userAvatar": "file:///android_asset/avatars/68.jpg",
"text": "ocurreret dico 📀⏺ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374130,
"reply": null,
"poll": null
}
],
"createdAt": 1636965174130,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "c60c3892-a8d8-4cb8-858b-1a8533e8ebf1",
"userNickname": "@conclu",
"userFullName": "Willis Lowery",
"userAvatar": "file:///android_asset/avatars/22.jpg",
"text": "⛱ viris tincidunt🦗🔩 ",
"images": [],
"likes": 59,
"replyCount": 83,
"messages": 0,
"comments": [],
"createdAt": 1637011974130,
"reply": {
"id": "e1f1596e-1706-4ac3-9c5f-1631f5333cb5",
"userNickname": "@homero",
"userFullName": "Mauricio Perry",
"userAvatar": "file:///android_asset/avatars/83.jpg",
"text": "🤢\nQuaestioveniam tractatos sapientem sanctus graecis harum legere ubique fabellas ipsum definiebas necessitatibus duis reprehendunt qui iisque eleifend. Interduminvenire utinam partiendo mi laoreet definitiones quaerendum deterruisset. Scelerisquenostrum ceteros.\n",
"images": [
"file:///android_asset/post_images/29.jpg",
"file:///android_asset/post_images/41.jpg",
"file:///android_asset/post_images/82.jpg",
"file:///android_asset/post_images/50.jpg",
"file:///android_asset/post_images/76.jpg"
],
"likes": 171,
"replyCount": 697,
"messages": 5,
"comments": [
{
"id": "c8b632f2-4126-4a3c-969f-dc710f00b797",
"userNickname": "@in",
"userFullName": "Myra Burnett",
"userAvatar": "file:///android_asset/avatars/103.jpg",
"text": "🎥📆 sententiae 🗂🔬 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574130,
"reply": null,
"poll": null
},
{
"id": "71774129-6aa3-4611-8244-85c5115914f0",
"userNickname": "@mei",
"userFullName": "Whitney Stokes",
"userAvatar": "file:///android_asset/avatars/110.jpg",
"text": "😅🍁😈 Vestibulummalorum habeo nam. Laboresnascetur lobortis ocurreret molestie pretium has nascetur aptent. Elaboraretcontentiones fabulas iusto reprimique. Antiopamcum sapien movet duo porttitor necessitatibus minim volumus vidisse moderatius noster efficitur posse suas epicurei dictum hac eruditi. Brutesuscipiantur hendrerit deterruisset iuvaret adhuc suas docendi dolor class eloquentiam fames congue quot populo antiopam omittantur maiorum. ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374131,
"reply": null,
"poll": null
},
{
"id": "df4b18bc-5a55-4de1-8838-da951ab56da2",
"userNickname": "@sceler",
"userFullName": "Ofelia Holman",
"userAvatar": "file:///android_asset/avatars/173.jpg",
"text": "petentium atomorum \n🈲 interdum voluptaria tritani 🌶 🍂🌉 ultricies \n💌 🔊🍾 consectetuer 🚟📦 🙂🍼 brute 🍸 🚉🔶 invenire 🌟🚶 ius doming \n💟 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974131,
"reply": null,
"poll": null
},
{
"id": "d56172da-59a7-4155-9ea6-f7646d415a9d",
"userNickname": "@lobort",
"userFullName": "Jim Rosario",
"userAvatar": "file:///android_asset/avatars/126.jpg",
"text": "Populovoluptatibus scripta purus expetendis consul delicata detracto non fabulas suas gravida. Tractatossaperet iuvaret dolore sententiae vel appetere. Auctorpossit interesset platonem sem legimus quisque feugait agam antiopam animal epicuri. Ultriciesnumquam amet posse reque omittam tota suas fabellas himenaeos eloquentiam ei liber feugiat repudiandae simul nascetur omittam atqui. Sapiendeseruisse interpretaris scripta quaerendum honestatis saperet ius eirmod amet option perpetua civibus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174131,
"reply": null,
"poll": null
},
{
"id": "96a78c41-4394-48ac-9a79-b02e14862469",
"userNickname": "@molest",
"userFullName": "Doyle Stanton",
"userAvatar": "file:///android_asset/avatars/39.jpg",
"text": "aperiri",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174131,
"reply": null,
"poll": null
}
],
"createdAt": 1636997574130,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "53a86ee9-e06f-4c08-bffc-77aa49ce846f",
"userNickname": "@posse",
"userFullName": "Corine Serrano",
"userAvatar": "file:///android_asset/avatars/121.jpg",
"text": "👩🎤🚙\nno aliquid maecenas🤣🌯📓\n",
"images": [],
"likes": 49,
"replyCount": 99,
"messages": 0,
"comments": [],
"createdAt": 1637011974131,
"reply": {
"id": "2e0c5980-20c6-4934-8f8f-9f76d82964c6",
"userNickname": "@volupt",
"userFullName": "Pauline Charles",
"userAvatar": "file:///android_asset/avatars/140.jpg",
"text": "📻🗑 Causaecursus graeco molestie accusata aenean feugait. Posteafringilla libris ius tota singulis sit mentitum offendit tristique impetus tale lectus lorem. Splendidefaucibus cetero cubilia simul ubique sumo mollis eros adversarium definitionem. Efficiturpropriae causae augue vix proin cursus parturient libris a senserit. Musiisque nunc.😀 ",
"images": [
"file:///android_asset/post_images/55.jpg",
"file:///android_asset/post_images/7.jpg"
],
"likes": 930,
"replyCount": 539,
"messages": 7,
"comments": [
{
"id": "5f18d78d-5222-46e8-a3de-b108a5b8c1c0",
"userNickname": "@luctus",
"userFullName": "Kurt Cantrell",
"userAvatar": "file:///android_asset/avatars/168.jpg",
"text": "quaerendum hinc 🦅 fringilla cetero 💵🌯 🍄💇♂ vel 🛂🍘 🆕🗞 veritus \n🌯 🕑📙 nonumes \n📑🥘 regione adolescens voluptatum \n🚕 🍘✌️ magna \n🦅 🐹🚙 curae 🍇 dis dolorem \n👨👧👧 ⚙️🏭 eirmod \n🎍👨🍳 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174131,
"reply": null,
"poll": null
},
{
"id": "4d850bda-1c24-411a-bf66-fe8fa5e9a29a",
"userNickname": "@suavit",
"userFullName": "Arnold Strickland",
"userAvatar": "file:///android_asset/avatars/33.jpg",
"text": "electram corrumpit adolescens",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174131,
"reply": null,
"poll": null
},
{
"id": "7d53312a-9dba-43fb-b36e-c27522523e7c",
"userNickname": "@dicta",
"userFullName": "Lucille Watkins",
"userAvatar": "file:///android_asset/avatars/25.jpg",
"text": "Augueappetere antiopam pellentesque impetus audire ultrices tortor auctor ancillae scripta pro delicata class lectus interdum legere. Posidoniumsententiae libero mi usu detraxit bibendum mnesarchum.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174131,
"reply": null,
"poll": null
},
{
"id": "21df954d-7c47-45b4-bd1a-8716463b9e6a",
"userNickname": "@inimic",
"userFullName": "Clair Gilbert",
"userAvatar": "file:///android_asset/avatars/149.jpg",
"text": "Ubiqueconsul inceptos gravida maiestatis sea finibus dictas taciti noster est posuere detraxit constituto vim deseruisse. Repudiareadhuc iaculis cubilia cras evertitur maximus. Cubiliadocendi prodesset eu vestibulum dicit sonet sale nibh sententiae suspendisse. Scelerisquerecteque vulputate fames mediocritatem fabellas suscipit latine pro magnis accusata utamur hendrerit placerat dolores noluisse mauris. Definiebascurae platea suspendisse propriae sapien interdum malesuada eirmod tritani discere contentiones inimicus euismod iusto. Interpretarissociis viderer ridiculus honestatis vituperatoribus aliquip.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174131,
"reply": null,
"poll": null
},
{
"id": "b28653ef-677a-462f-a0d8-6ba238edef32",
"userNickname": "@defini",
"userFullName": "Francisco Swanson",
"userAvatar": "file:///android_asset/avatars/0.jpg",
"text": "voluptatibus adipisci definitiones \n🚇🌒 at usu \n⚜️😞 💰🧣 dolorum 🚭🕵 epicurei legere necessitatibus 🍳 cubilia tincidunt \n📁 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974131,
"reply": null,
"poll": null
},
{
"id": "c36e78e2-2ab9-4999-964f-03fe86a5257c",
"userNickname": "@delect",
"userFullName": "Lacey Cochran",
"userAvatar": "file:///android_asset/avatars/75.jpg",
"text": "🌑🚋 volumus \n🍵🚢 ullamcorper vulputate urna \n⭐️🌃 🎢😧 nominavi \n🕣 salutatus potenti \n🦅 🚛🍝 hinc 🍮👩🏫 😒🎉 facilis 🎂 partiendo necessitatibus scripserit \n👭👨👧👦 😲♊️ neque \n⛽️ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374131,
"reply": null,
"poll": null
},
{
"id": "0eed87cb-94b3-453c-b31a-4cf41047ce90",
"userNickname": "@graece",
"userFullName": "Karin Conley",
"userAvatar": "file:///android_asset/avatars/108.jpg",
"text": "elitr qui mucius",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374131,
"reply": null,
"poll": null
}
],
"createdAt": 1637008374131,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "4ee7d924-895d-4e5a-8e0d-0d45de4b481c",
"userNickname": "@aliqui",
"userFullName": "Miranda Robinson",
"userAvatar": "file:///android_asset/avatars/77.jpg",
"text": "📺👩👩👦💔 sumo pericula voluptatum\n",
"images": [],
"likes": 8,
"replyCount": 144,
"messages": 0,
"comments": [],
"createdAt": 1636990374131,
"reply": {
"id": "c8a6a07c-4cc0-484e-9d3d-71a445a5bc16",
"userNickname": "@litora",
"userFullName": "Jason Melton",
"userAvatar": "file:///android_asset/avatars/79.jpg",
"text": "🐖\nNecessitatibuspri parturient posidonium legimus quisque urna. Vocentdiscere id et feugait mel scripta facilisis quisque reprehendunt erroribus error ferri deseruisse.🏪\n",
"images": [
"file:///android_asset/post_images/17.jpg",
"file:///android_asset/post_images/25.jpg"
],
"likes": 849,
"replyCount": 453,
"messages": 6,
"comments": [
{
"id": "122a5f3d-16a5-4d90-bfe8-1f186e58b321",
"userNickname": "@impetu",
"userFullName": "Stacy Reed",
"userAvatar": "file:///android_asset/avatars/100.jpg",
"text": "⛩🆘 malesuada ipsum ne ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774132,
"reply": null,
"poll": null
},
{
"id": "8e33a0ea-a30b-4f4e-8c3d-4c08b8b8e811",
"userNickname": "@partie",
"userFullName": "Dollie Holland",
"userAvatar": "file:///android_asset/avatars/92.jpg",
"text": "🦏🤶 facilisi 🍂 solet montes 🌀🍕 🏕🖼 ridens \n🦃 🚶♀🚥 invenire \n🚲🍆 🗽🌒 fames \n👩🚒 ⛲️👨⚕ eius \n🦇 evertitur suspendisse \n🍙 graece faucibus \n🚽💪 magna corrumpit nominavi 👨✈ ♒️🚡 noster \n🕹 🍮🃏 epicurei \n📓 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774132,
"reply": null,
"poll": null
},
{
"id": "69f628af-7c43-4d91-bfef-3905085ad380",
"userNickname": "@eleife",
"userFullName": "Amos Farmer",
"userAvatar": "file:///android_asset/avatars/195.jpg",
"text": "Honestatispri intellegebat urbanitas tantas parturient condimentum adipiscing delicata populo iaculis sea errem morbi ad iriure reque reprimique ligula constituto. Nasceturaffert legere melius. Sapientemut suscipit condimentum iuvaret et deseruisse. Portaindoctum per tristique placerat iudicabit dolorum morbi fabulas persequeris imperdiet non vituperata propriae suspendisse nihil volumus suas neque. Dolorintellegebat similique tamquam adipiscing aptent atqui deseruisse eloquentiam utinam veritus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374132,
"reply": null,
"poll": null
},
{
"id": "c00b3ebc-84ec-4a8b-af54-f60055911ce7",
"userNickname": "@iriure",
"userFullName": "Tamra Warner",
"userAvatar": "file:///android_asset/avatars/194.jpg",
"text": "🏦🛫🎋 Penatibusipsum faucibus vituperata legere adhuc erat dicat vestibulum gloriatur id menandri. Ignotaeloquentiam agam accumsan wisi leo disputationi partiendo. Duiproin suscipit id vis patrioque option expetendis tristique no erroribus saepe inani utroque causae commodo.\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374132,
"reply": null,
"poll": null
},
{
"id": "6398eed7-9b71-47f1-9671-12fcd79a2d08",
"userNickname": "@dolore",
"userFullName": "Claudette Bright",
"userAvatar": "file:///android_asset/avatars/95.jpg",
"text": "fames volumus iudicabit",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174132,
"reply": null,
"poll": null
},
{
"id": "cff914b5-3081-4ee3-9241-d950bd70849d",
"userNickname": "@portti",
"userFullName": "Lorraine Bush",
"userAvatar": "file:///android_asset/avatars/74.jpg",
"text": "🚜📰🔐\nFallierror partiendo mauris cetero curae errem. Adhucei ei vituperata esse semper sumo torquent detracto. Soletdicunt voluptatum ludus ligula condimentum arcu posuere.🍹🍻✒️ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174132,
"reply": null,
"poll": null
}
],
"createdAt": 1636975974131,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "786ce361-4e41-4708-9f93-169ed1c4b45a",
"userNickname": "@nullam",
"userFullName": "Wilmer Barrera",
"userAvatar": "file:///android_asset/avatars/84.jpg",
"text": "patrioque",
"images": [],
"likes": 357,
"replyCount": 506,
"messages": 9,
"comments": [
{
"id": "1b8ae1b2-2623-439d-b953-206e10bd4432",
"userNickname": "@alique",
"userFullName": "Rusty Olson",
"userAvatar": "file:///android_asset/avatars/36.jpg",
"text": "🆚\nSuaslaoreet quo aliquid et ponderum volutpat elit. Nisiplatonem accumsan vivendo aptent veniam torquent dico interesset vestibulum iriure intellegebat ex reprimique sodales. Quampellentesque mus pharetra sumo imperdiet quas deseruisse constituam eget splendide autem pretium sagittis accusata recteque menandri errem. Dicamaliquam inceptos delenit suspendisse no mucius dis dicit idque. Meaaccommodare turpis no inciderint tellus arcu pericula detraxit tellus suspendisse civibus.🚾 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174132,
"reply": null,
"poll": null
},
{
"id": "d59c4afa-621d-4094-90b0-610044f6a299",
"userNickname": "@tantas",
"userFullName": "Duane Warner",
"userAvatar": "file:///android_asset/avatars/8.jpg",
"text": "Offenditvoluptatibus pharetra sociis volutpat error fabulas constituam contentiones blandit minim aptent congue fermentum utroque vis. Definiebasdictum periculis molestie. Contentionesquaerendum definiebas causae veri eius quidam aperiri omittantur consequat corrumpit mei eius ponderum. Ludusferri fastidii tibique oporteat debet aliquip qualisque nibh. Requepostulant senectus elaboraret magnis percipit suspendisse vocent pertinax.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374132,
"reply": null,
"poll": null
},
{
"id": "81bb3dfa-47b1-467f-96ea-327f67b4099c",
"userNickname": "@possit",
"userFullName": "Leila Flynn",
"userAvatar": "file:///android_asset/avatars/96.jpg",
"text": "scripserit similique",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374132,
"reply": null,
"poll": null
},
{
"id": "74ca5162-fa3b-4ec3-a14d-f273c8b2b888",
"userNickname": "@nihil",
"userFullName": "Seth Burke",
"userAvatar": "file:///android_asset/avatars/52.jpg",
"text": "quaeque dapibus pretium felis metus",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974132,
"reply": null,
"poll": null
},
{
"id": "9256de24-ee06-486e-b66d-ad66d3f9b38a",
"userNickname": "@recteq",
"userFullName": "Garth McCarty",
"userAvatar": "file:///android_asset/avatars/184.jpg",
"text": "↕️ Theophrastusanimal tale rutrum dolor eruditi dicat consequat nullam libero oporteat molestie dolore elaboraret interpretaris quem mazim consetetur. Estetiam patrioque offendit mi invenire tale natum voluptaria. Taleproin saepe reformidans ac euripidis quaeque sadipscing. Gubergrenrecteque diam usu eum constituam natoque nec habitant vulputate volumus egestas imperdiet adolescens.🎋 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774132,
"reply": null,
"poll": null
},
{
"id": "70480e60-1133-49bb-bb7c-ed3e835b6ede",
"userNickname": "@adipis",
"userFullName": "Horace Franks",
"userAvatar": "file:///android_asset/avatars/50.jpg",
"text": "📅🥦🗿\ntibique augue🙅♂😉\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574132,
"reply": null,
"poll": null
},
{
"id": "87b01179-78ad-4db1-ba22-321438c5f7ce",
"userNickname": "@habemu",
"userFullName": "Reynaldo Fletcher",
"userAvatar": "file:///android_asset/avatars/44.jpg",
"text": "📹🚓 accusata \n🔖 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174132,
"reply": null,
"poll": null
},
{
"id": "69ae5d6d-5b66-4f6a-aaab-242d254a5c00",
"userNickname": "@penati",
"userFullName": "Francis Trevino",
"userAvatar": "file:///android_asset/avatars/53.jpg",
"text": "♍️👨👦🏔 indoctum cubilia quidam🥠😰\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774132,
"reply": null,
"poll": null
},
{
"id": "8acdfcbe-ebad-4041-8606-5ab8058f221c",
"userNickname": "@everti",
"userFullName": "Dexter Maddox",
"userAvatar": "file:///android_asset/avatars/160.jpg",
"text": "Graecipossim iusto. Torquentdefinitiones altera curae quod latine tacimates lectus cetero tantas facilisi persecuti per duo.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974133,
"reply": null,
"poll": null
}
],
"createdAt": 1637008374132,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 1014,
"positions": [
{
"text": "homero ornare ridiculus ponderum dictum",
"voted": 18
},
{
"text": "delenit dignissim repudiare ius legimus",
"voted": 123
},
{
"text": "nullam mollis constituto",
"voted": 405
},
{
"text": "invenire nostrum",
"voted": 468
}
]
}
},
{
"id": "4bf06ee7-c0c0-4f20-8e49-f3cb2291761c",
"userNickname": "@sem",
"userFullName": "Ahmad Huff",
"userAvatar": "file:///android_asset/avatars/183.jpg",
"text": "Duobrute euripidis et principes adversarium vocent petentium nisi reprimique vocibus quot prodesset theophrastus aptent mutat. Sumodoming utinam nonumy etiam mandamus elitr nullam vel. Minimius duis periculis.",
"images": [
"file:///android_asset/post_images/42.jpg"
],
"likes": 344,
"replyCount": 456,
"messages": 7,
"comments": [
{
"id": "a309f0e0-92ed-4235-9aef-3704b8ea43ff",
"userNickname": "@lobort",
"userFullName": "Everette Lamb",
"userAvatar": "file:///android_asset/avatars/87.jpg",
"text": "dicam has menandri",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374133,
"reply": null,
"poll": null
},
{
"id": "0ea24634-2e85-4c56-9e05-4072dbc0968e",
"userNickname": "@conval",
"userFullName": "Rod Wilkins",
"userAvatar": "file:///android_asset/avatars/122.jpg",
"text": "🍋🌗 Intellegebatconsequat verear taciti mei graece non theophrastus verterem nonumy nascetur gubergren bibendum te ut convenire. Sanctusmassa omittantur varius dicam ligula idque nonumes nobis wisi altera veritus convallis ad.\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174133,
"reply": null,
"poll": null
},
{
"id": "0a014bfa-fffb-4f03-8342-498451a965d1",
"userNickname": "@mollis",
"userFullName": "Marcos Phillips",
"userAvatar": "file:///android_asset/avatars/81.jpg",
"text": "👲💇♂🌏 Malesuadaagam natoque sale iusto adolescens congue. Aptentsodales magna in porro quem sapientem magna wisi.🕴\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774133,
"reply": null,
"poll": null
},
{
"id": "5678cf7b-faad-4859-b382-ba7ccea65363",
"userNickname": "@utroqu",
"userFullName": "Fabian Charles",
"userAvatar": "file:///android_asset/avatars/73.jpg",
"text": "enim",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974133,
"reply": null,
"poll": null
},
{
"id": "d95d0265-bc7c-4ef1-97e4-3bf7e1c19f39",
"userNickname": "@eius",
"userFullName": "Phillip England",
"userAvatar": "file:///android_asset/avatars/164.jpg",
"text": "eu placerat pretium",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974133,
"reply": null,
"poll": null
},
{
"id": "a1af296f-1f37-46cf-b40d-9c6bf995f26c",
"userNickname": "@simili",
"userFullName": "Marlon Hanson",
"userAvatar": "file:///android_asset/avatars/99.jpg",
"text": "ancillae possim sonet nec",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774133,
"reply": null,
"poll": null
},
{
"id": "b542afd6-57c2-447d-b146-3ffe5cb506be",
"userNickname": "@solet",
"userFullName": "Elwood Manning",
"userAvatar": "file:///android_asset/avatars/30.jpg",
"text": "at",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774133,
"reply": null,
"poll": null
}
],
"createdAt": 1637008374133,
"reply": null,
"poll": null
},
{
"id": "102d2e4f-b2e7-4183-ac1a-7b808f331ac5",
"userNickname": "@ex",
"userFullName": "Alton Petty",
"userAvatar": "file:///android_asset/avatars/113.jpg",
"text": "qui mutat aperiri \n👱🌅 ",
"images": [],
"likes": 73,
"replyCount": 179,
"messages": 0,
"comments": [],
"createdAt": 1636986774133,
"reply": {
"id": "8f043c6e-fa5d-42ca-8a34-ef2587c4a6ac",
"userNickname": "@natoqu",
"userFullName": "Leah Nicholson",
"userAvatar": "file:///android_asset/avatars/32.jpg",
"text": "🍶🕸\nBlanditeum explicari nihil persecuti scripserit dissentiunt mediocritatem graece. Antereque atomorum solet errem morbi sapientem simul iusto voluptatum. Verteremfinibus molestiae justo mea malesuada quas constituam mus elit veniam postea adipiscing. Nonumesmelius tincidunt tation inani eloquentiam. Inanidefinitiones legimus ultrices expetendis alia amet utroque voluptaria vidisse fermentum pertinax dicam te dissentiunt urbanitas quot ligula dicit dicant.🏠🐿 ",
"images": [],
"likes": 69,
"replyCount": 656,
"messages": 2,
"comments": [
{
"id": "5cd900c8-1d2f-435b-8a67-ece5f0201763",
"userNickname": "@phasel",
"userFullName": "Kris Galloway",
"userAvatar": "file:///android_asset/avatars/176.jpg",
"text": "Feugiatmalorum tacimates dicta ex deterruisset vel ocurreret commune invidunt lacinia reprimique vituperatoribus odio instructior. Ultricesvocent oratio impetus te graeco dicit persius sollicitudin solet solum aeque consectetuer tota penatibus a luptatum postea vocibus. Requequam finibus delectus cursus felis scripserit voluptaria assueverit dicam. Risuspercipit sadipscing pro civibus leo id sociosqu.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374133,
"reply": null,
"poll": null
},
{
"id": "57629c73-7f94-4f46-8ff8-92a6a36b1b43",
"userNickname": "@habemu",
"userFullName": "Levi Yang",
"userAvatar": "file:///android_asset/avatars/49.jpg",
"text": "👦💀 malesuada \n🌆 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374133,
"reply": null,
"poll": null
}
],
"createdAt": 1636986774133,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "b37fd49e-c6b9-41f4-8fc3-3edc9248cadc",
"userNickname": "@himena",
"userFullName": "Andre Long",
"userAvatar": "file:///android_asset/avatars/116.jpg",
"text": "Porttitorius sumo feugiat feugiat adolescens commune electram gloriatur interpretaris consul duis dolor maluisset dicta nihil tamquam decore. Quaestiooratio a solum fusce eros intellegebat praesent urna suavitate viverra leo quas fabulas senectus dicat harum constituam. Luptatumancillae faucibus porro suavitate oratio has eam no ad error simul senectus contentiones nunc eu vivendo. Malesuadaornatus nonumes mucius suas decore assueverit usu sollicitudin euripidis adversarium efficitur purus deseruisse alienum gloriatur vocent pretium mandamus imperdiet.",
"images": [
"file:///android_asset/post_images/45.jpg",
"file:///android_asset/post_images/60.jpg",
"file:///android_asset/post_images/74.jpg",
"file:///android_asset/post_images/66.jpg"
],
"likes": 49,
"replyCount": 582,
"messages": 5,
"comments": [
{
"id": "5fb56fc1-66f2-4a40-a61f-608002f34a71",
"userNickname": "@negleg",
"userFullName": "Howard Bird",
"userAvatar": "file:///android_asset/avatars/54.jpg",
"text": "💤👷♀ facilis \n⌛️ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774133,
"reply": null,
"poll": null
},
{
"id": "5b9e8330-a2dd-48f3-bec6-ce0efff902dd",
"userNickname": "@fabell",
"userFullName": "Ethel Bowen",
"userAvatar": "file:///android_asset/avatars/14.jpg",
"text": "eros aeque \n🍿💊 🦊🍽 nihil 🗻 ➕™️ quam \n👡 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774133,
"reply": null,
"poll": null
},
{
"id": "9337e202-854e-449a-a882-1a6a6a9a9a3b",
"userNickname": "@sadips",
"userFullName": "Valentin Chavez",
"userAvatar": "file:///android_asset/avatars/9.jpg",
"text": "sollicitudin lectus prodesset singulis",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974133,
"reply": null,
"poll": null
},
{
"id": "a2c2bb6e-0e4e-4e40-a089-f3c5f7dd0f7a",
"userNickname": "@dicam",
"userFullName": "Reba Sears",
"userAvatar": "file:///android_asset/avatars/143.jpg",
"text": "postulant mollis",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374134,
"reply": null,
"poll": null
},
{
"id": "783e5c81-f343-4275-b2d0-8904aeb0b373",
"userNickname": "@everti",
"userFullName": "Helena Lindsey",
"userAvatar": "file:///android_asset/avatars/174.jpg",
"text": "magna vel \n🍌🐋 saperet eloquentiam tale \n🌕🌱 posuere eros ligula 🐦 💱🖊 consetetur \n🔴🍗 prompta oporteat facilis 🗒🐉 atqui malorum \n💵 ♠️✳️ verterem \n🐂 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774134,
"reply": null,
"poll": null
}
],
"createdAt": 1636929174133,
"reply": null,
"poll": null
},
{
"id": "885c360a-3723-4627-b315-64f1aaef8e0a",
"userNickname": "@interp",
"userFullName": "Boyd Woods",
"userAvatar": "file:///android_asset/avatars/193.jpg",
"text": "🌈🚒🦁 egestas vidisse🍊🗓🐣\n",
"images": [],
"likes": 33,
"replyCount": 37,
"messages": 0,
"comments": [],
"createdAt": 1636932774134,
"reply": {
"id": "522cbe28-8b3f-43c2-813f-a7eb9a7792e2",
"userNickname": "@vel",
"userFullName": "Earlene Ball",
"userAvatar": "file:///android_asset/avatars/42.jpg",
"text": "🌎☝️🌡\nPorttitorsingulis dictumst fabulas brute regione maiorum sapien odio. Corrumpitreque omittantur nobis eirmod praesent ancillae corrumpit elaboraret delicata dicunt id maximus ultrices populo mnesarchum semper molestie. Inciderinteruditi cetero finibus convenire utroque alienum regione solet primis quaestio maecenas mediocritatem deserunt mus signiferumque dis fuisset prompta. Erroribusignota comprehensam melius efficitur ornare persius proin eu inciderint praesent.🔯🚆\n",
"images": [],
"likes": 490,
"replyCount": 17,
"messages": 7,
"comments": [
{
"id": "07b5ebac-402f-4b86-96a2-9c1cb736c098",
"userNickname": "@vulput",
"userFullName": "Austin Wilkins",
"userAvatar": "file:///android_asset/avatars/166.jpg",
"text": "Vulputatepatrioque volumus causae. Audireiusto semper alia usu taciti conceptam. Librispro eirmod necessitatibus iuvaret dolore pulvinar suscipiantur metus suavitate. Accommodareius magnis sapientem mauris causae etiam adipiscing mutat.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774134,
"reply": null,
"poll": null
},
{
"id": "c305ca64-072e-4b65-a3d5-bcd45270f65c",
"userNickname": "@offend",
"userFullName": "Jeri Stewart",
"userAvatar": "file:///android_asset/avatars/125.jpg",
"text": "aperiri molestie cras 🌾💩 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974134,
"reply": null,
"poll": null
},
{
"id": "4b8367ca-8089-4540-9ad5-93c70dbb3a80",
"userNickname": "@disput",
"userFullName": "Barton Swanson",
"userAvatar": "file:///android_asset/avatars/192.jpg",
"text": "Wisidefinitionem cras. Dictaquisque lobortis altera impetus pertinacia imperdiet has eu neglegentur a repudiare erat venenatis equidem epicurei iuvaret. Expetendaoratio discere wisi convenire posidonium nihil. Iuvaretdapibus alterum tamquam discere senectus constituam epicurei arcu debet libero vix urbanitas molestiae urbanitas. Aliaquidam latine convallis utroque expetenda maximus theophrastus ligula idque non inceptos utamur dicat curabitur eleifend quis nunc tibique definiebas.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574134,
"reply": null,
"poll": null
},
{
"id": "05d43465-ba00-4f01-a250-d3aefe82ed21",
"userNickname": "@sanctu",
"userFullName": "Rosemary Hood",
"userAvatar": "file:///android_asset/avatars/188.jpg",
"text": "🈳👢 in \n🔐🥠 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974134,
"reply": null,
"poll": null
},
{
"id": "a3c1ad34-a213-4a9b-b686-174ca27ab883",
"userNickname": "@urna",
"userFullName": "Aimee Hood",
"userAvatar": "file:///android_asset/avatars/117.jpg",
"text": "labores",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174134,
"reply": null,
"poll": null
},
{
"id": "d7579ea2-c8d9-42cf-8b61-40876f502128",
"userNickname": "@dui",
"userFullName": "Leah Foster",
"userAvatar": "file:///android_asset/avatars/10.jpg",
"text": "🥦🥐 quaeque 🐖 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974134,
"reply": null,
"poll": null
},
{
"id": "80776b94-02bd-4d9c-9da7-fd0fa79057a2",
"userNickname": "@detrac",
"userFullName": "Leola Potter",
"userAvatar": "file:///android_asset/avatars/115.jpg",
"text": "Docendidiscere gravida aptent. Loremelementum vocibus vituperatoribus cu habemus assueverit regione equidem periculis. Penatibusdicat theophrastus vim mucius hac ultricies esse sapientem finibus. Quodluctus inani partiendo quas. Melmalorum cubilia praesent indoctum tation viderer quidam comprehensam elaboraret fuisset habitasse tation hinc perpetua. Quidetraxit tortor voluptatum erroribus voluptaria pri noluisse efficitur autem consul interpretaris habemus periculis.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974134,
"reply": null,
"poll": null
}
],
"createdAt": 1636932774134,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "15d8ce90-27c9-4233-a288-ef686f14008f",
"userNickname": "@orci",
"userFullName": "Ruben Chandler",
"userAvatar": "file:///android_asset/avatars/3.jpg",
"text": "Senectusodio partiendo ac maecenas et aperiri idque delenit esse varius dicam lorem justo varius ignota class. Mediocrempossit est eos wisi. Ultriciesluptatum omittam persequeris viverra maiorum veri at ipsum errem nulla saperet reformidans aliquip debet posse. Signiferumquepropriae ac ferri cubilia odio gravida habitasse definitiones altera neglegentur inani habitant sea. Admaiestatis veri interesset debet urbanitas invenire qualisque impetus accumsan pertinacia definiebas dicam erroribus. Adipiscidebet option viverra suas tantas laudem adipisci massa utamur dico nisl reque commune fugit mel ubique prompta.",
"images": [],
"likes": 449,
"replyCount": 72,
"messages": 6,
"comments": [
{
"id": "c88a0265-e185-4660-8be0-0efd5cfa669e",
"userNickname": "@iaculi",
"userFullName": "Felecia Hampton",
"userAvatar": "file:///android_asset/avatars/154.jpg",
"text": "💠🔰\nVidissepersius tractatos finibus gubergren tacimates interdum reformidans. Eroserror delicata curae civibus finibus ornare oratio eruditi minim tractatos. Atquitantas nulla habemus recteque altera leo pericula ornare quot. Duispartiendo civibus electram efficiantur. Omnesqueerroribus alia maiorum nonumes referrentur curabitur efficiantur pellentesque tellus fermentum quot ridiculus sadipscing consectetur postea quas elitr. Convenireequidem eros viderer mus quaerendum gravida eget. ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974134,
"reply": null,
"poll": null
},
{
"id": "e4ef5b33-ae70-4e43-be0a-8801eaba774e",
"userNickname": "@facili",
"userFullName": "Roman Gill",
"userAvatar": "file:///android_asset/avatars/133.jpg",
"text": "Omittanturte corrumpit lacus ei fringilla est taciti convenire populo homero agam. Idquelaudem offendit ullamcorper movet platonem prompta assueverit. Proinveri ante nascetur venenatis liber venenatis petentium pro gloriatur aperiri cetero a deseruisse causae. Noluissepri ponderum has has suas molestiae brute integer tempor tibique discere mus vituperata sem oratio sale felis consectetuer ridiculus. Atquicomprehensam definitiones percipit moderatius inani appetere commune montes nascetur mollis. Promptamollis montes minim inani est invidunt.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974134,
"reply": null,
"poll": null
},
{
"id": "c238a894-bf39-450e-b293-0140b649bee9",
"userNickname": "@fermen",
"userFullName": "Bobbie Spence",
"userAvatar": "file:///android_asset/avatars/57.jpg",
"text": "malesuada velit aliquam dictumst definiebas",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574134,
"reply": null,
"poll": null
},
{
"id": "f5f8ca5a-50d7-4ca8-aaab-58a29c63f8d8",
"userNickname": "@odio",
"userFullName": "Terra Hancock",
"userAvatar": "file:///android_asset/avatars/35.jpg",
"text": "porttitor lorem suas 🌺🕚 🏕📘 moderatius \n🍇💝 📴🍣 causae 🍬♨️ 📞🗒 sociis \n🧣 🐌🐚 sumo 🏬🌍 turpis petentium aliquam 😅 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774134,
"reply": null,
"poll": null
},
{
"id": "d1fc53ed-ec68-450e-b57c-1ee27472eed1",
"userNickname": "@enim",
"userFullName": "Douglas Manning",
"userAvatar": "file:///android_asset/avatars/15.jpg",
"text": "nihil vim nec",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374134,
"reply": null,
"poll": null
},
{
"id": "5882a73f-c9e6-4376-a0e9-38a63bd62912",
"userNickname": "@inani",
"userFullName": "Carolina Logan",
"userAvatar": "file:///android_asset/avatars/179.jpg",
"text": "🕍🚆\nmandamus maecenas quis a ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374134,
"reply": null,
"poll": null
}
],
"createdAt": 1636943574134,
"reply": null,
"poll": null
},
{
"id": "518e4fbe-c42a-4d42-8a6e-b4116dfd722f",
"userNickname": "@quisqu",
"userFullName": "Rolando Shepherd",
"userAvatar": "file:///android_asset/avatars/127.jpg",
"text": "fusce commune",
"images": [],
"likes": 41,
"replyCount": 7,
"messages": 0,
"comments": [],
"createdAt": 1636968774134,
"reply": {
"id": "e69e8290-941c-4102-a8a3-2c1277cac2a3",
"userNickname": "@dapibu",
"userFullName": "Lorenzo Campos",
"userAvatar": "file:///android_asset/avatars/185.jpg",
"text": "Sollicitudinullamcorper menandri. Tritanimandamus augue feugait facilisi aeque ipsum nonumes primis deseruisse quod. Aeneanalterum dictumst convallis reque venenatis aptent molestiae velit voluptatibus malorum delectus. Vocentsem aliquet mollis lectus legimus mus luptatum graece semper habitant magna ad.",
"images": [
"file:///android_asset/post_images/47.jpg",
"file:///android_asset/post_images/94.jpg",
"file:///android_asset/post_images/11.jpg"
],
"likes": 58,
"replyCount": 194,
"messages": 3,
"comments": [
{
"id": "3aed902b-6a10-448f-be46-396264039d78",
"userNickname": "@saluta",
"userFullName": "Jasper Stanton",
"userAvatar": "file:///android_asset/avatars/78.jpg",
"text": "Totarepudiandae reprimique dicat adhuc sed vivendo posuere instructior. Lacusaltera eos viris ornare putent efficitur. Delectuspertinax regione maiestatis mnesarchum harum conceptam at iuvaret tritani neglegentur erroribus vocibus varius luctus. Dictasconsequat rutrum populo eripuit voluptatibus mus. Muspersecuti fuisset theophrastus expetenda noster maecenas phasellus ludus ignota sale.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374135,
"reply": null,
"poll": null
},
{
"id": "0a9b8800-79f8-4657-909c-91ac9be791e6",
"userNickname": "@luptat",
"userFullName": "Donna Pollard",
"userAvatar": "file:///android_asset/avatars/29.jpg",
"text": "nisi omittam \n🛸 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774135,
"reply": null,
"poll": null
},
{
"id": "f97bc0b1-83d3-42a1-bcc0-3850320d9fea",
"userNickname": "@neque",
"userFullName": "Lara Castro",
"userAvatar": "file:///android_asset/avatars/93.jpg",
"text": "Mattishomero referrentur maximus affert platea antiopam sonet comprehensam ridens eget gravida signiferumque tation sonet. Civibuspersius molestie lacus persecuti ante tempor. Finibusmei utamur salutatus meliore. Tevoluptatum sapien nostra patrioque mea option delicata voluptaria quas definitiones class arcu sodales impetus dictum sollicitudin evertitur sonet. Rhoncusinterdum vehicula bibendum utroque.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574135,
"reply": null,
"poll": null
}
],
"createdAt": 1636965174134,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "459696a5-8801-4464-bbd0-fe012e6b2ef3",
"userNickname": "@tota",
"userFullName": "Reginald Davidson",
"userAvatar": "file:///android_asset/avatars/21.jpg",
"text": "dico",
"images": [],
"likes": 168,
"replyCount": 345,
"messages": 5,
"comments": [
{
"id": "4a52f3ec-a38c-451d-b43c-cc0f773fc295",
"userNickname": "@eirmod",
"userFullName": "Monty Velez",
"userAvatar": "file:///android_asset/avatars/72.jpg",
"text": "nascetur vis mollis 🍺🚦 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374135,
"reply": null,
"poll": null
},
{
"id": "2ace4ca6-ca1d-45aa-a65e-64053486d9e6",
"userNickname": "@iaculi",
"userFullName": "Sam Juarez",
"userAvatar": "file:///android_asset/avatars/198.jpg",
"text": "🌹🤠\nbibendum💦 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574135,
"reply": null,
"poll": null
},
{
"id": "1fa693f6-34ab-48c1-92d5-7ab689e81fb3",
"userNickname": "@cu",
"userFullName": "Jake Delaney",
"userAvatar": "file:///android_asset/avatars/46.jpg",
"text": "Duivulputate error aliquet graeco dicta iaculis fusce phasellus. Himenaeospulvinar sit reprimique appareat urna dicta risus. Dapibussit elaboraret epicuri habitant omittam ultrices aenean mnesarchum leo sollicitudin litora omittam commodo docendi gravida eius. Aliquammus ignota placerat verear.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774135,
"reply": null,
"poll": null
},
{
"id": "1d47d0f0-082c-40c1-b87c-af0db31cc257",
"userNickname": "@affert",
"userFullName": "Clare Hartman",
"userAvatar": "file:///android_asset/avatars/5.jpg",
"text": "suas dicit ornare facilisi malorum",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374135,
"reply": null,
"poll": null
},
{
"id": "18b03d7e-719c-40d8-9a9f-7b3dc71434e0",
"userNickname": "@vel",
"userFullName": "Rickey Patel",
"userAvatar": "file:///android_asset/avatars/162.jpg",
"text": "Eratcommune dolore periculis euripidis dolore erroribus natum metus aenean nibh postea quaerendum veritus saepe voluptatum laoreet euismod scripta auctor. Conclusionemqueignota debet imperdiet efficitur. Repudiandaesenserit detraxit nibh appareat convenire orci. Dolorempericula laudem solet altera malesuada senserit mei autem instructior tempus falli.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574135,
"reply": null,
"poll": null
}
],
"createdAt": 1636939974135,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 1120,
"positions": [
{
"text": "falli deterruisset tractatos sodales detraxit",
"voted": 267
},
{
"text": "tempor altera pertinacia",
"voted": 466
},
{
"text": "senectus iudicabit utinam ea",
"voted": 142
},
{
"text": "nisi conclusionemque dapibus",
"voted": 158
},
{
"text": "wisi risus",
"voted": 87
}
]
}
},
{
"id": "3ea8c272-d083-4cc6-8267-cf5d5a01d021",
"userNickname": "@laudem",
"userFullName": "Gladys Martin",
"userAvatar": "file:///android_asset/avatars/180.jpg",
"text": "💶⏳ Semvoluptatibus ipsum suas quisque. Habeodisputationi ne ludus tibique maecenas verear inceptos persius latine intellegebat sale graece luctus sonet repudiare singulis conubia turpis sociosqu.\n",
"images": [
"file:///android_asset/post_images/14.jpg"
],
"likes": 522,
"replyCount": 487,
"messages": 4,
"comments": [
{
"id": "cab167e0-f951-4936-abcc-57bf1bd37be6",
"userNickname": "@simul",
"userFullName": "Greta Conley",
"userAvatar": "file:///android_asset/avatars/66.jpg",
"text": "Aptentconceptam mea. Aperiricontentiones fugit suscipit interpretaris sollicitudin risus accommodare tale graeci dolores ei convenire corrumpit graeci fastidii.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774135,
"reply": null,
"poll": null
},
{
"id": "cab56bbe-15ba-4407-bb28-c66357d5a28d",
"userNickname": "@duis",
"userFullName": "Tania Leblanc",
"userAvatar": "file:///android_asset/avatars/76.jpg",
"text": "🥫🏖 et ⏭ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774135,
"reply": null,
"poll": null
},
{
"id": "16a8cb2b-2e5f-4af2-ba4a-aff60fabea7d",
"userNickname": "@habemu",
"userFullName": "Lemuel Reyes",
"userAvatar": "file:///android_asset/avatars/86.jpg",
"text": "legere egestas error \n⏱↕️ 🎒▫️ assueverit 🎓🐄 🈹📀 habitasse \n🥌 👼🏘 penatibus 👩✈ 🔒🙊 agam 🥒 dicunt efficiantur 🌸📯 🚘🍫 vim \n🍃🍢 📁🍑 putent 🥌 consectetuer tibique litora \n📩 🐟🛎 propriae \n🍑 🐇🦃 pericula \n🐓 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574135,
"reply": null,
"poll": null
},
{
"id": "378347b2-f8e1-439d-81a7-19aad17442f9",
"userNickname": "@omitta",
"userFullName": "Miranda Callahan",
"userAvatar": "file:///android_asset/avatars/90.jpg",
"text": "Inceptospersecuti platonem parturient maximus. Molestiehabemus cursus mollis persius melius quas dico his dolore iisque. Singulisepicurei donec dolorem ornare dui gloriatur esse nostra salutatus consequat usu noster eros.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174135,
"reply": null,
"poll": null
}
],
"createdAt": 1636979574135,
"reply": null,
"poll": null
},
{
"id": "c750dc62-7fb1-43dd-9519-f9d1143076de",
"userNickname": "@conval",
"userFullName": "Lloyd Castro",
"userAvatar": "file:///android_asset/avatars/105.jpg",
"text": "🍡🎴 interesset ex rhoncus hac🈂️\n",
"images": [],
"likes": 81,
"replyCount": 18,
"messages": 0,
"comments": [],
"createdAt": 1637011974136,
"reply": {
"id": "e89d9e74-0f92-496b-b206-4b7cec3bcb82",
"userNickname": "@conset",
"userFullName": "Maria Wilson",
"userAvatar": "file:///android_asset/avatars/107.jpg",
"text": "Commodocondimentum suavitate ridiculus tempor. Viscras simul tantas consetetur aeque alia lorem his nonumes mediocrem cum agam cum morbi melius nonumes. Mnesarchumgraecis interpretaris accusata suscipiantur impetus magna repudiare corrumpit scripta iisque delenit offendit lacinia mediocritatem gravida enim epicurei deserunt. Primisaliquet invenire sonet neque elitr eu bibendum ancillae civibus dolore non petentium noster. Appeterepellentesque natum ultrices suas pulvinar intellegat ancillae fringilla interpretaris quaeque molestiae. Oporteatsapientem assueverit arcu dolorem malorum dolorum suavitate expetendis reprehendunt neque lectus sapientem corrumpit.",
"images": [],
"likes": 166,
"replyCount": 218,
"messages": 6,
"comments": [
{
"id": "97fb486a-3503-40ef-b633-89fc225487d1",
"userNickname": "@himena",
"userFullName": "Pierre McGowan",
"userAvatar": "file:///android_asset/avatars/175.jpg",
"text": "movet delenit suas",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374135,
"reply": null,
"poll": null
},
{
"id": "3bec4ba5-27de-490b-a360-f81c6edeab99",
"userNickname": "@fermen",
"userFullName": "Joann Kennedy",
"userAvatar": "file:///android_asset/avatars/145.jpg",
"text": "🤳☯️ Appareatfames ex dignissim in ocurreret adolescens fastidii litora. Miharum mazim legimus persecuti. Eiushinc melius aptent quas pellentesque graecis facilis simul.⚛️🚞🥟 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574135,
"reply": null,
"poll": null
},
{
"id": "cd133d87-e1be-4bd9-ae82-f3ce303d1338",
"userNickname": "@oporte",
"userFullName": "Dino Neal",
"userAvatar": "file:///android_asset/avatars/55.jpg",
"text": "🍋🐐\neuismod suscipiantur🦃🌾🚗 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374136,
"reply": null,
"poll": null
},
{
"id": "6e517f8d-deb7-45a1-b075-a7a9f30da9db",
"userNickname": "@lacus",
"userFullName": "Rae Rios",
"userAvatar": "file:///android_asset/avatars/23.jpg",
"text": "✨🥒\nLaoreeterrem primis intellegat neque proin volumus dolorem noster viderer has offendit quo vituperatoribus eget eos pro dissentiunt latine. Felisadhuc eget libris. Alterumsolum dolorem ferri ridens explicari tale tibique.👽🍼\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374136,
"reply": null,
"poll": null
},
{
"id": "bba47c19-d3d6-4a33-8b00-8f07ba610138",
"userNickname": "@maximu",
"userFullName": "Reyna Tucker",
"userAvatar": "file:///android_asset/avatars/159.jpg",
"text": "🕷 Verteremrutrum ac nec oratio mnesarchum condimentum. Wisicras sociosqu referrentur utamur graeco referrentur habeo. Iusdisputationi primis porttitor ne quas definitionem accommodare principes dicunt novum noster mi mea. Verearquem omnesque inciderint mea aeque iudicabit alienum saepe maluisset vivamus perpetua egestas.👧🍙\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574136,
"reply": null,
"poll": null
},
{
"id": "ca41f744-2872-429e-b683-07556d96d786",
"userNickname": "@vehicu",
"userFullName": "Erwin Stevenson",
"userAvatar": "file:///android_asset/avatars/132.jpg",
"text": "Putentaltera suscipit porttitor purus. Atinimicus nec vel ceteros prodesset tempus ornare. Dissentiuntsadipscing amet aeque ridiculus maximus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974136,
"reply": null,
"poll": null
}
],
"createdAt": 1637004774135,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "af6f19fe-bf49-4503-b8f3-f6e57e785600",
"userNickname": "@quot",
"userFullName": "Frederic Spence",
"userAvatar": "file:///android_asset/avatars/39.jpg",
"text": "🛀\nmagnis adolescens quaestio invidunt tractatos🚘 ",
"images": [],
"likes": 18,
"replyCount": 152,
"messages": 0,
"comments": [],
"createdAt": 1636993974136,
"reply": {
"id": "d20088b5-09bf-473d-a97f-0dbcb6f86420",
"userNickname": "@mus",
"userFullName": "Stevie Maynard",
"userAvatar": "file:///android_asset/avatars/64.jpg",
"text": "Atquierror liber constituam aliquam amet. Facilisimolestiae sem vim usu corrumpit putent nam sonet fermentum nostrum odio senectus nascetur his feugait nisi maiestatis tritani movet. Nullamet sea suavitate splendide repudiare deseruisse iaculis habitant sanctus nonumy pretium dicat dolorum porro conclusionemque ornare prompta. Epicuriinteger metus euripidis.",
"images": [],
"likes": 370,
"replyCount": 633,
"messages": 6,
"comments": [
{
"id": "e886234e-4a53-4f25-ac2d-bb6a4ad49789",
"userNickname": "@movet",
"userFullName": "Percy Spencer",
"userAvatar": "file:///android_asset/avatars/59.jpg",
"text": "sed recteque",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574136,
"reply": null,
"poll": null
},
{
"id": "bb60aa12-5631-44f6-93da-c47b98e881fc",
"userNickname": "@ridens",
"userFullName": "Amos Hays",
"userAvatar": "file:///android_asset/avatars/50.jpg",
"text": "Expetendismollis tamquam parturient intellegebat iisque harum leo nunc dicta magnis persius scripta agam gloriatur noluisse ancillae. Tristiquesuscipiantur interdum ea definiebas molestiae suspendisse facilisi affert duo habeo ipsum tota aenean quidam neque melius dicat. Necdelenit ac vidisse pertinax tincidunt utinam nec ut. Sedaudire urbanitas error inceptos expetenda molestie habitasse sapien turpis contentiones. Vimmandamus dictas iudicabit pellentesque qui. Tationei alienum verear delenit sadipscing docendi salutatus viderer viderer leo per sit commodo omnesque.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374136,
"reply": null,
"poll": null
},
{
"id": "e55c5caa-c879-45e1-9228-a6bf8cfa30ca",
"userNickname": "@repudi",
"userFullName": "Milton Church",
"userAvatar": "file:///android_asset/avatars/51.jpg",
"text": "Iaculissodales antiopam. Dissentiuntreferrentur feugait principes montes dictumst enim litora quaeque parturient imperdiet fringilla dictum delenit. Urbanitasturpis natoque ultricies. Estsimul nullam augue constituto tale et brute liber mea utinam impetus interesset. Adversariumlatine verear brute voluptaria eirmod moderatius eos adhuc persequeris elementum aenean labores simul pri. Mnesarchumid eleifend eripuit postea.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974136,
"reply": null,
"poll": null
},
{
"id": "c37e2d1e-81bd-4da9-aeeb-2ec9b8694f6c",
"userNickname": "@tacima",
"userFullName": "Romeo Bauer",
"userAvatar": "file:///android_asset/avatars/3.jpg",
"text": "velit conceptam quis montes morbi",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374136,
"reply": null,
"poll": null
},
{
"id": "3fbee157-e21f-42b1-a979-62c8af9227f9",
"userNickname": "@medioc",
"userFullName": "Luella Ortega",
"userAvatar": "file:///android_asset/avatars/148.jpg",
"text": "Referrenturconstituam odio verear partiendo nec ex constituto urbanitas mea persius enim vix torquent autem orci dignissim. Phasellusmattis minim tractatos auctor voluptatum accumsan dictas congue reformidans possim inciderint vestibulum inceptos quas vehicula appareat. Persecutiridens mutat ne faucibus prompta interpretaris ne vocent etiam aeque splendide sit graeci appetere discere mediocrem aptent graeco odio. Conguelegere porro debet persecuti curae possim doctus neque reque alia porro natum gubergren sem reque volutpat splendide. Duisodales dicunt indoctum urbanitas suscipit etiam voluptatum vocent novum habitant pellentesque sadipscing sem.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374136,
"reply": null,
"poll": null
},
{
"id": "096a2e9b-1173-469e-bd23-511ad9b084de",
"userNickname": "@deseru",
"userFullName": "Alphonso Watson",
"userAvatar": "file:///android_asset/avatars/116.jpg",
"text": "💂 Cursusconclusionemque nominavi sapientem homero. Fringillapharetra voluptatum velit interdum oratio rutrum minim.🐩 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174136,
"reply": null,
"poll": null
}
],
"createdAt": 1636979574136,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "e57fc765-87d1-46e7-80c5-aff6aefae609",
"userNickname": "@ornare",
"userFullName": "Ben Russo",
"userAvatar": "file:///android_asset/avatars/50.jpg",
"text": "elaboraret ignota natoque",
"images": [],
"likes": 119,
"replyCount": 364,
"messages": 5,
"comments": [
{
"id": "b91e4fff-6be6-43f4-9257-53e871511358",
"userNickname": "@negleg",
"userFullName": "James Hays",
"userAvatar": "file:///android_asset/avatars/82.jpg",
"text": "utinam persecuti \n🕊 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974137,
"reply": null,
"poll": null
},
{
"id": "464538b2-a903-412e-9408-6ed505e45d0d",
"userNickname": "@utroqu",
"userFullName": "Sebastian Gonzalez",
"userAvatar": "file:///android_asset/avatars/10.jpg",
"text": "♐️🌎\nquaestio sollicitudin ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574137,
"reply": null,
"poll": null
},
{
"id": "d45b825d-c328-45a8-84c7-4043c90acd70",
"userNickname": "@aliqui",
"userFullName": "Yvonne Hawkins",
"userAvatar": "file:///android_asset/avatars/148.jpg",
"text": "Facilisismazim sententiae ferri dapibus invidunt persius natoque dictum cubilia. Gubergrenlacus rhoncus libero malesuada doming porttitor purus utroque reprimique veritus ea nec netus eloquentiam.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374137,
"reply": null,
"poll": null
},
{
"id": "72736599-0e0d-49f2-9b54-016fcd1defa1",
"userNickname": "@oratio",
"userFullName": "Kelly Olsen",
"userAvatar": "file:///android_asset/avatars/110.jpg",
"text": "⁉️➡️ tellus \n🛢👭 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574137,
"reply": null,
"poll": null
},
{
"id": "cbb0493e-8a95-4c8a-a691-75b9cfee0b89",
"userNickname": "@simili",
"userFullName": "Elinor Moran",
"userAvatar": "file:///android_asset/avatars/162.jpg",
"text": "saperet nascetur omittantur 🍺🐍 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374137,
"reply": null,
"poll": null
}
],
"createdAt": 1636979574136,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 351,
"positions": [
{
"text": "urna dictas phasellus",
"voted": 128
},
{
"text": "dui regione doctus persequeris",
"voted": 63
},
{
"text": "iusto",
"voted": 160
}
]
}
},
{
"id": "9891b32f-a843-439a-96b1-9fd81b14ecc8",
"userNickname": "@adoles",
"userFullName": "Sanford Allen",
"userAvatar": "file:///android_asset/avatars/147.jpg",
"text": "repudiandae",
"images": [],
"likes": 609,
"replyCount": 43,
"messages": 3,
"comments": [
{
"id": "6a46ba9f-49d0-4292-aebb-23ef9b21a3e1",
"userNickname": "@nisi",
"userFullName": "Lamont Frederick",
"userAvatar": "file:///android_asset/avatars/111.jpg",
"text": "omittantur",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574137,
"reply": null,
"poll": null
},
{
"id": "2aba3888-b139-4dcf-9bd6-051b14d852d4",
"userNickname": "@suscip",
"userFullName": "Tamika Tate",
"userAvatar": "file:///android_asset/avatars/11.jpg",
"text": "lobortis movet utamur",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574137,
"reply": null,
"poll": null
},
{
"id": "38248467-3b1f-43cc-a9e3-1b7f08ff0b16",
"userNickname": "@consec",
"userFullName": "Levi Dominguez",
"userAvatar": "file:///android_asset/avatars/36.jpg",
"text": "Hascondimentum atomorum gravida sea aliquid mentitum cras definitiones antiopam mi possim labores condimentum. Alienumcongue iudicabit usu delenit dicit vulputate habitant diam posidonium legere cras necessitatibus epicurei assueverit. Quisquemagna definitionem a. Mediocremharum netus debet elaboraret malorum ius aenean.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774137,
"reply": null,
"poll": null
}
],
"createdAt": 1636950774137,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 1080,
"positions": [
{
"text": "luptatum laudem",
"voted": 167
},
{
"text": "utroque hinc mnesarchum pro",
"voted": 377
},
{
"text": "mel utinam imperdiet principes",
"voted": 345
},
{
"text": "maximus pri eloquentiam",
"voted": 171
},
{
"text": "lacinia iaculis duis malorum",
"voted": 20
}
]
}
},
{
"id": "ac5be1be-59db-4090-ae29-28c91a6b3e2b",
"userNickname": "@aenean",
"userFullName": "Jill Booker",
"userAvatar": "file:///android_asset/avatars/171.jpg",
"text": "🐜 nonumy vituperata utroque👧\n",
"images": [],
"likes": 3,
"replyCount": 199,
"messages": 0,
"comments": [],
"createdAt": 1636975974137,
"reply": {
"id": "5f6291fd-2547-4119-8b86-70c7a14afc2f",
"userNickname": "@deseru",
"userFullName": "Corrine Gordon",
"userAvatar": "file:///android_asset/avatars/134.jpg",
"text": "pretium sociis pericula \n🔱🐻 📬💉 viverra \n🛣🚟 👩🎤🐑 utinam \n🤒⛴ te mazim 💜🖍 iriure usu ultrices \n🔻⏹ ",
"images": [
"file:///android_asset/post_images/59.jpg",
"file:///android_asset/post_images/13.jpg",
"file:///android_asset/post_images/48.jpg",
"file:///android_asset/post_images/92.jpg"
],
"likes": 974,
"replyCount": 698,
"messages": 4,
"comments": [
{
"id": "3f34c23e-d111-4d9e-940e-a099f11563d6",
"userNickname": "@iusto",
"userFullName": "German Preston",
"userAvatar": "file:///android_asset/avatars/192.jpg",
"text": "ac fugit affert feugait",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174137,
"reply": null,
"poll": null
},
{
"id": "29102eff-89d0-4df7-bc3f-896528a034dc",
"userNickname": "@senser",
"userFullName": "Roberta Clay",
"userAvatar": "file:///android_asset/avatars/183.jpg",
"text": "ludus facilisi iudicabit",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774137,
"reply": null,
"poll": null
},
{
"id": "b2fad259-59ab-4b68-97a0-b5061f1769eb",
"userNickname": "@vel",
"userFullName": "Carlene Carney",
"userAvatar": "file:///android_asset/avatars/118.jpg",
"text": "🍘\nscripserit quaeque quaeque📹 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574137,
"reply": null,
"poll": null
},
{
"id": "68130072-eb70-46a6-aa03-91d5578f207c",
"userNickname": "@aperir",
"userFullName": "Sharon Burnett",
"userAvatar": "file:///android_asset/avatars/158.jpg",
"text": "percipit",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774137,
"reply": null,
"poll": null
}
],
"createdAt": 1636968774137,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "23b633f6-551b-40fc-82e1-6df5cdf4b6a7",
"userNickname": "@alique",
"userFullName": "Orville Preston",
"userAvatar": "file:///android_asset/avatars/110.jpg",
"text": "🈯️\nassueverit pharetra prodesset vel🧣🐻🦇 ",
"images": [],
"likes": 16,
"replyCount": 120,
"messages": 0,
"comments": [],
"createdAt": 1636957974138,
"reply": {
"id": "b9f8a988-cbd8-46bd-8f7b-07293ec51e46",
"userNickname": "@condim",
"userFullName": "Bethany Melton",
"userAvatar": "file:///android_asset/avatars/131.jpg",
"text": "errem condimentum suscipit 🌴🌠 graece quam \n🌯 per pharetra antiopam 🚙 pellentesque omnesque vulputate 🎅 periculis vituperatoribus 👗😭 📦🚇 wisi \n⭐️ habitant omittantur 👺 an debet periculis 🥛 ",
"images": [],
"likes": 588,
"replyCount": 352,
"messages": 7,
"comments": [
{
"id": "af4dc6a9-1113-4b72-97fc-3d165dfd245f",
"userNickname": "@repreh",
"userFullName": "Don Long",
"userAvatar": "file:///android_asset/avatars/56.jpg",
"text": "🍼👮♀🦔 electram delenit justo tritani👿🦖👨👦👦\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574138,
"reply": null,
"poll": null
},
{
"id": "012330c5-ccd0-4676-9e51-459dbcf4c40d",
"userNickname": "@vis",
"userFullName": "Ingrid Dickson",
"userAvatar": "file:///android_asset/avatars/165.jpg",
"text": "habitant sociosqu",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774138,
"reply": null,
"poll": null
},
{
"id": "20a8a530-a696-4b1c-8b9d-ee0c3d4b2eb1",
"userNickname": "@habeo",
"userFullName": "Bobbie Glenn",
"userAvatar": "file:///android_asset/avatars/180.jpg",
"text": "sea fames accumsan inceptos",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974138,
"reply": null,
"poll": null
},
{
"id": "e54e84d5-a730-42c9-a915-621375743045",
"userNickname": "@vim",
"userFullName": "Guy Ware",
"userAvatar": "file:///android_asset/avatars/122.jpg",
"text": "🥚🙆♂ aperiri \n😋 vitae fuisset ad 🐼🌤 🗺🌻 maiorum ☂️💴 movet latine alterum \n🥔🙋♂ 🏘👩💻 eloquentiam 🚊 expetendis luctus 📳 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974138,
"reply": null,
"poll": null
},
{
"id": "80505e2c-58fe-481d-9352-8d1af072e62c",
"userNickname": "@ridens",
"userFullName": "Gloria Pratt",
"userAvatar": "file:///android_asset/avatars/11.jpg",
"text": "nominavi",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974138,
"reply": null,
"poll": null
},
{
"id": "ffc8dfcb-69ff-4f6c-98e6-085cb3a4ff81",
"userNickname": "@fermen",
"userFullName": "Patrick Shields",
"userAvatar": "file:///android_asset/avatars/178.jpg",
"text": "🛷💢🍮 Eleifendcivibus intellegat possim arcu omittantur signiferumque suscipiantur tamquam errem constituto fabulas. Malorumcommune aliquip verterem habeo sententiae ridiculus voluptaria consectetur dicant intellegat. Quemgraece expetendis conceptam quod quaeque eloquentiam discere pretium proin repudiandae luctus autem iriure auctor fabulas gloriatur. Mollistibique integer propriae. Nullafaucibus error semper nullam definitiones. Molestieomittantur graecis wisi ignota aeque saepe referrentur eam utamur nam.💇 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774138,
"reply": null,
"poll": null
},
{
"id": "19f08eb2-771a-48f9-9a66-5155173452f6",
"userNickname": "@atqui",
"userFullName": "Neva Fernandez",
"userAvatar": "file:///android_asset/avatars/188.jpg",
"text": "sadipscing neque 🔩👗 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374138,
"reply": null,
"poll": null
}
],
"createdAt": 1636950774138,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "e93d358c-eade-43f8-8fd8-5dad935f8a96",
"userNickname": "@lacini",
"userFullName": "Conrad Santana",
"userAvatar": "file:///android_asset/avatars/150.jpg",
"text": "Vivamusappareat porta eleifend luctus graeci labores pharetra risus sumo solum melius cu animal similique dui. Montesnumquam litora prompta eleifend meliore risus definitionem antiopam affert interesset vehicula sanctus altera interesset egestas sapientem.",
"images": [],
"likes": 266,
"replyCount": 202,
"messages": 8,
"comments": [
{
"id": "af2b4194-afe0-480d-a7e7-1faf3e5872e5",
"userNickname": "@errem",
"userFullName": "Dawn Schmidt",
"userAvatar": "file:///android_asset/avatars/177.jpg",
"text": "qui cras lorem sea",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974138,
"reply": null,
"poll": null
},
{
"id": "83adcea4-64ee-47a2-a22f-2bb95f6bcc57",
"userNickname": "@vocibu",
"userFullName": "Jeanette Rivera",
"userAvatar": "file:///android_asset/avatars/177.jpg",
"text": "Ponderumridiculus platonem latine reprimique pertinax alterum. Tibiquesumo quod sumo aliquid quisque sea autem ancillae elitr pericula porro vidisse detraxit evertitur parturient postulant porttitor ornare. Salehabeo suspendisse commune arcu nonumes tation posidonium tractatos graecis dicunt definitiones pulvinar mus. Conubiadecore inciderint aliquip debet quam nostra his mucius vel phasellus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574138,
"reply": null,
"poll": null
},
{
"id": "8e7610aa-ff41-4a69-8961-3dd28e4479ab",
"userNickname": "@pertin",
"userFullName": "Ivory Ball",
"userAvatar": "file:///android_asset/avatars/94.jpg",
"text": "nobis suspendisse conubia \n🍁📿 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374138,
"reply": null,
"poll": null
},
{
"id": "525e6d3e-fcc8-4c3d-8fc5-c9b8cc1eda5e",
"userNickname": "@ponder",
"userFullName": "Darryl Woodard",
"userAvatar": "file:///android_asset/avatars/134.jpg",
"text": "fames vituperatoribus",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374138,
"reply": null,
"poll": null
},
{
"id": "90ef0e14-eca5-40e9-b9c5-9b7b4fdc230d",
"userNickname": "@egesta",
"userFullName": "Alfredo Dejesus",
"userAvatar": "file:///android_asset/avatars/192.jpg",
"text": "🛋👩👧\nposidonium justo\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574138,
"reply": null,
"poll": null
},
{
"id": "c0045829-89e1-4350-84e4-f78313bfef0b",
"userNickname": "@singul",
"userFullName": "Danial Kirkland",
"userAvatar": "file:///android_asset/avatars/83.jpg",
"text": "🥃\ngubergren omittam tota inimicus laoreet🔥 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774138,
"reply": null,
"poll": null
},
{
"id": "b20b7e54-482f-49a6-8d6f-e2dd5fdc0d6d",
"userNickname": "@nisi",
"userFullName": "Stacy Cameron",
"userAvatar": "file:///android_asset/avatars/51.jpg",
"text": "🐱🔑🚮\nVelitultrices donec usu atqui ponderum per cras wisi tortor etiam. Causaeblandit primis invenire libero solet possit ne homero molestie autem convenire nostra sonet suscipiantur non omnesque delicata altera delenit. Erataliquip labores expetenda moderatius disputationi repudiandae. Decoremus viris pretium elaboraret numquam principes graece explicari.🚑 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774138,
"reply": null,
"poll": null
},
{
"id": "f8d90b10-cca7-49cc-af6c-7a6d0a4cd4c7",
"userNickname": "@phasel",
"userFullName": "Desiree Pratt",
"userAvatar": "file:///android_asset/avatars/121.jpg",
"text": "praesent morbi suscipiantur 🐫🍿 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374138,
"reply": null,
"poll": null
}
],
"createdAt": 1636947174138,
"reply": null,
"poll": null
},
{
"id": "28fff591-27c3-47a4-bdd1-1ccb72c01799",
"userNickname": "@vel",
"userFullName": "Jerome Sandoval",
"userAvatar": "file:///android_asset/avatars/58.jpg",
"text": "📥🈂️ posidonium petentium dicant sed😨💧\n",
"images": [],
"likes": 25,
"replyCount": 145,
"messages": 0,
"comments": [],
"createdAt": 1636950774138,
"reply": {
"id": "528e10f8-0b29-4808-9bb6-2790979e092d",
"userNickname": "@fabell",
"userFullName": "Patti Hammond",
"userAvatar": "file:///android_asset/avatars/74.jpg",
"text": "☕️🚫➡️\nLeopossim vim suscipit eirmod intellegebat ipsum reprehendunt. Possecum indoctum referrentur expetenda quis.📮🍇 ",
"images": [],
"likes": 973,
"replyCount": 696,
"messages": 6,
"comments": [
{
"id": "f513574a-8453-4211-a07e-0d77465c6d21",
"userNickname": "@semper",
"userFullName": "Dale Howe",
"userAvatar": "file:///android_asset/avatars/74.jpg",
"text": "Aptentrecteque tale morbi menandri veri varius perpetua in litora accommodare definiebas et quot porta pulvinar. Condimentumefficiantur mandamus mi tibique voluptaria prodesset dicam oporteat antiopam velit legere. Prosolum quaeque arcu novum donec ipsum erroribus tempor utamur harum est quisque taciti ne patrioque saperet civibus delenit nihil. Agamgubergren nisi ultricies doctus honestatis nascetur ancillae. Quemperpetua efficitur quaeque neglegentur magnis accommodare lacus vivamus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174138,
"reply": null,
"poll": null
},
{
"id": "69fe2f6f-ba49-4924-ad86-ce9ef90e4f6b",
"userNickname": "@altera",
"userFullName": "Alden Head",
"userAvatar": "file:///android_asset/avatars/41.jpg",
"text": "🌹👾 primis 🦕🌘 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974139,
"reply": null,
"poll": null
},
{
"id": "d027cf4e-d324-4edc-9c0a-b40e0b46f670",
"userNickname": "@suas",
"userFullName": "Jesus Mercado",
"userAvatar": "file:///android_asset/avatars/9.jpg",
"text": "🍗👩👦 maiorum \n🚎 dolores autem gravida \n🍔🍛 🔋🐿 accumsan \n🍭 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174139,
"reply": null,
"poll": null
},
{
"id": "5b175447-7c7d-44a2-8d74-987f44e25e58",
"userNickname": "@molest",
"userFullName": "Marty Jenkins",
"userAvatar": "file:///android_asset/avatars/74.jpg",
"text": "Veniamexpetendis eget mazim ne blandit inimicus accusata necessitatibus integer per. Intellegatdui arcu feugiat decore vituperata maluisset suspendisse. Causaemei postulant sollicitudin. Himenaeoshonestatis condimentum ferri sadipscing dicit iuvaret malesuada usu vituperatoribus arcu. Habeoturpis vel luctus dico constituam ac iusto tractatos ubique aliquip. Eaodio quas petentium vivendo interesset himenaeos magna senectus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974139,
"reply": null,
"poll": null
},
{
"id": "c22cd54a-ec70-4026-a163-d6f45d61a7cf",
"userNickname": "@mollis",
"userFullName": "Enrique Davenport",
"userAvatar": "file:///android_asset/avatars/89.jpg",
"text": "👽🥝🔢 an salutatus\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574139,
"reply": null,
"poll": null
},
{
"id": "25b03045-79d4-4b75-8991-3743c11726c4",
"userNickname": "@ponder",
"userFullName": "Vance Underwood",
"userAvatar": "file:///android_asset/avatars/85.jpg",
"text": "consectetuer civibus \n㊗️ utinam tempus 🙏🎄 💢🦉 iudicabit 🗒🚬 duo elitr invidunt 🐥 quaeque inimicus \n💛🍩 🏯🐒 qualisque \n🍱 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374139,
"reply": null,
"poll": null
}
],
"createdAt": 1636950774138,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "0f204a16-eaac-41ad-86ac-d314a9646bd6",
"userNickname": "@vidiss",
"userFullName": "Clinton Moss",
"userAvatar": "file:///android_asset/avatars/168.jpg",
"text": "sapientem maecenas \n⚗️ ",
"images": [],
"likes": 743,
"replyCount": 596,
"messages": 7,
"comments": [
{
"id": "48bca3dc-80bb-479a-bd6f-9ef277e29a07",
"userNickname": "@tamqua",
"userFullName": "Harriet Warner",
"userAvatar": "file:///android_asset/avatars/77.jpg",
"text": "👠⤴️ curae \n🍑 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574139,
"reply": null,
"poll": null
},
{
"id": "3537eeec-5592-463a-871c-4556ab763048",
"userNickname": "@socios",
"userFullName": "Glenna Fitzgerald",
"userAvatar": "file:///android_asset/avatars/0.jpg",
"text": "🎠🚧 legimus 😢 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774139,
"reply": null,
"poll": null
},
{
"id": "40fd3639-1c3d-446e-8cc6-ebff4fde00c0",
"userNickname": "@vocent",
"userFullName": "Cameron Galloway",
"userAvatar": "file:///android_asset/avatars/115.jpg",
"text": "Cumdignissim dolore nihil iriure interpretaris convenire consectetur netus utroque vehicula causae donec augue latine voluptaria option. Hisodio graeco appareat phasellus suavitate viderer lacinia faucibus eos tempus metus dolor congue urna orci sapientem saperet. Tritanisea voluptatibus inimicus adolescens quaestio mediocritatem graece quisque dictumst idque sale hendrerit civibus scelerisque. Efficianturnoluisse detracto adipisci simul postulant moderatius eirmod penatibus at brute facilisi.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174139,
"reply": null,
"poll": null
},
{
"id": "b69b620d-8111-4c17-a032-57a0cdc87131",
"userNickname": "@sonet",
"userFullName": "Elvin Calhoun",
"userAvatar": "file:///android_asset/avatars/191.jpg",
"text": "🍎🍥💁♂\nnunc maecenas referrentur ultrices🍺🥤\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174139,
"reply": null,
"poll": null
},
{
"id": "fcd581c5-747e-4e49-ad03-77c5885cccac",
"userNickname": "@esse",
"userFullName": "Tami Mann",
"userAvatar": "file:///android_asset/avatars/148.jpg",
"text": "Vocentsit convenire. Acper ludus atomorum lectus noster integer efficitur diam etiam habitant nulla blandit graece mei eum eget menandri decore. Urnaadversarium singulis pulvinar melius discere tantas vivamus convenire deseruisse sit alia consectetur solum vocent euripidis mandamus diam laoreet nisl.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374139,
"reply": null,
"poll": null
},
{
"id": "3104c2b7-0b56-44dc-9a6b-ccac9acc84c8",
"userNickname": "@urna",
"userFullName": "Bernadette Duran",
"userAvatar": "file:///android_asset/avatars/96.jpg",
"text": "fames pertinacia interesset docendi",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974139,
"reply": null,
"poll": null
},
{
"id": "a1095a4b-520f-4df8-aba1-cf51974dc75c",
"userNickname": "@ea",
"userFullName": "Major Stein",
"userAvatar": "file:///android_asset/avatars/9.jpg",
"text": "Esterat dapibus ponderum dicunt vero epicurei quaestio diam vidisse vivamus usu. Velnunc wisi lorem saepe deseruisse mauris.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174139,
"reply": null,
"poll": null
}
],
"createdAt": 1636950774139,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 1113,
"positions": [
{
"text": "vis moderatius minim",
"voted": 483
},
{
"text": "persius vivamus risus falli",
"voted": 337
},
{
"text": "non",
"voted": 222
},
{
"text": "duis semper",
"voted": 29
},
{
"text": "nulla sumo",
"voted": 42
}
]
}
},
{
"id": "8331cffc-c0d8-46b0-b0ee-56bd4b0f5309",
"userNickname": "@legimu",
"userFullName": "Evangeline Ratliff",
"userAvatar": "file:///android_asset/avatars/59.jpg",
"text": "periculis ferri",
"images": [],
"likes": 3,
"replyCount": 5,
"messages": 0,
"comments": [],
"createdAt": 1636947174139,
"reply": {
"id": "c4920f4e-868e-43f9-a39b-e096885b06b3",
"userNickname": "@eu",
"userFullName": "Susie Huffman",
"userAvatar": "file:///android_asset/avatars/100.jpg",
"text": "Porroneque dictum inciderint ut scripta rhoncus lacinia mnesarchum elitr evertitur sapientem blandit. Maluissethabemus postulant neque pertinacia pericula maiorum appetere. Maximuseloquentiam sapien detraxit graeco proin commodo no ea agam nostra no luctus sale. Doloressenserit nisl ultricies faucibus nisl habitasse posidonium vim repudiandae constituam reprimique dis tristique theophrastus accommodare augue class ludus.",
"images": [],
"likes": 554,
"replyCount": 675,
"messages": 9,
"comments": [
{
"id": "84a22d2a-2e4a-4feb-b989-5be2c5c3e91f",
"userNickname": "@mollis",
"userFullName": "Dave Sharpe",
"userAvatar": "file:///android_asset/avatars/131.jpg",
"text": "tristique",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774139,
"reply": null,
"poll": null
},
{
"id": "4e63f4e7-fecf-45f0-9b6f-811be166a19e",
"userNickname": "@labore",
"userFullName": "Abe Black",
"userAvatar": "file:///android_asset/avatars/42.jpg",
"text": "Meaviverra urna donec velit odio quas auctor prompta intellegebat quis mauris pri option nunc nihil. Domingmenandri consectetur finibus meliore solet malesuada habitant suscipit feugiat invenire iaculis. Elitnominavi saperet veniam. Persecutiindoctum suscipit tractatos cu nam dolore maluisset non sonet fermentum usu appetere quod. Imperdietlegere no proin quisque pretium impetus feugait duis interdum tota iudicabit finibus invenire quas ultrices ancillae brute. Magnislobortis at tamquam viderer intellegebat sapien dictas ponderum inciderint assueverit tractatos suscipit invenire mauris eripuit.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374139,
"reply": null,
"poll": null
},
{
"id": "e2a68f71-8a4b-43e0-94ec-8893bae1e88d",
"userNickname": "@magna",
"userFullName": "Dick Potts",
"userAvatar": "file:///android_asset/avatars/194.jpg",
"text": "delicata ne expetenda \n🚡👨🍳 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974139,
"reply": null,
"poll": null
},
{
"id": "32a240d5-a71d-4e1a-8ba7-2b983ef27ac6",
"userNickname": "@errem",
"userFullName": "Israel Barrett",
"userAvatar": "file:///android_asset/avatars/161.jpg",
"text": "porttitor meliore falli \n⛑ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774139,
"reply": null,
"poll": null
},
{
"id": "5bc1098f-7fcf-4dfb-b5e8-dd51b97246b3",
"userNickname": "@praese",
"userFullName": "Blanca Leblanc",
"userAvatar": "file:///android_asset/avatars/49.jpg",
"text": "nec comprehensam finibus omittantur nostra",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174139,
"reply": null,
"poll": null
},
{
"id": "1850136a-2bbe-4769-9020-5af194438519",
"userNickname": "@omitta",
"userFullName": "Annabelle Roth",
"userAvatar": "file:///android_asset/avatars/152.jpg",
"text": "minim quidam",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974139,
"reply": null,
"poll": null
},
{
"id": "0fa96951-6356-4bf5-892c-69355e79a23a",
"userNickname": "@dolor",
"userFullName": "Marcy Douglas",
"userAvatar": "file:///android_asset/avatars/133.jpg",
"text": "utroque porro 🚟🌏 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574139,
"reply": null,
"poll": null
},
{
"id": "77b92719-deb2-4d5c-b81e-972fa6565eba",
"userNickname": "@ius",
"userFullName": "Sophie Boyd",
"userAvatar": "file:///android_asset/avatars/46.jpg",
"text": "impetus ac singulis causae habitasse",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774139,
"reply": null,
"poll": null
},
{
"id": "6ba5a6c8-90e7-49d1-8600-8a4f8a20617f",
"userNickname": "@veri",
"userFullName": "Willard Santos",
"userAvatar": "file:///android_asset/avatars/13.jpg",
"text": "🚌🐖 quisque 👖 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574140,
"reply": null,
"poll": null
}
],
"createdAt": 1636939974139,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "10da7f9c-6f5c-41f5-a83a-609a04160c43",
"userNickname": "@faucib",
"userFullName": "Briana Lloyd",
"userAvatar": "file:///android_asset/avatars/135.jpg",
"text": "🥔😋 ac 🏩 ",
"images": [],
"likes": 93,
"replyCount": 124,
"messages": 0,
"comments": [],
"createdAt": 1636979574140,
"reply": {
"id": "fd9cac03-da54-4443-b4f9-9a8bdd9c3a05",
"userNickname": "@habita",
"userFullName": "Elba Pickett",
"userAvatar": "file:///android_asset/avatars/146.jpg",
"text": "2️⃣🛰\nPetentiumnatum conclusionemque eruditi debet hac vulputate prodesset fuisset adversarium potenti harum nisl dissentiunt integer senectus appareat. Tantasmenandri ex inani scelerisque mel tacimates dico metus oporteat urna sit utroque elaboraret corrumpit. Ubiquedicit adolescens meliore animal viderer vitae viris doming ignota elit vulputate finibus posuere metus vis habitant maiestatis. Persiusdicam cum. Justoagam constituam id constituam persecuti agam fringilla posidonium facilisi quam voluptaria aliquet tota aliquet detracto nascetur tritani.🥝\n",
"images": [
"file:///android_asset/post_images/79.jpg"
],
"likes": 372,
"replyCount": 603,
"messages": 2,
"comments": [
{
"id": "39de84a4-7fc0-4648-96a6-3139707969c9",
"userNickname": "@vocibu",
"userFullName": "Harriett Pennington",
"userAvatar": "file:///android_asset/avatars/122.jpg",
"text": "eros nominavi \n🚞🔢 utroque rutrum \n🥖 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774140,
"reply": null,
"poll": null
},
{
"id": "c285bab5-9029-4efa-8901-ee5f356d63d7",
"userNickname": "@enim",
"userFullName": "Lillie Underwood",
"userAvatar": "file:///android_asset/avatars/39.jpg",
"text": "tempor mi",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774140,
"reply": null,
"poll": null
}
],
"createdAt": 1636972374140,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "f17772ac-6203-47a4-b7ac-0f3b99bc7789",
"userNickname": "@offend",
"userFullName": "Lucinda Ochoa",
"userAvatar": "file:///android_asset/avatars/126.jpg",
"text": "Tamquamsigniferumque reque altera tacimates vivamus singulis. Utfeugait graeci. Puruspossit class mediocritatem conceptam. Alteravituperata meliore pharetra qui nonumes scelerisque putent pellentesque aliquet.",
"images": [],
"likes": 48,
"replyCount": 60,
"messages": 7,
"comments": [
{
"id": "23d1f7b4-6666-42e6-8213-3dbf9ac53df2",
"userNickname": "@consul",
"userFullName": "Miguel Quinn",
"userAvatar": "file:///android_asset/avatars/12.jpg",
"text": "Congueaenean dolorem intellegat vestibulum dicat explicari atqui legimus noluisse moderatius ornare dignissim delectus. Luduscondimentum pri delectus scripta adversarium causae affert urna. Tinciduntaccommodare fastidii dignissim bibendum accommodare duo nisi eirmod partiendo verear oporteat vivendo. Tellusepicuri alienum varius odio deserunt primis erat dico neque eu.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174140,
"reply": null,
"poll": null
},
{
"id": "98c74ce3-9680-47e6-9f70-54062ac128e0",
"userNickname": "@lacus",
"userFullName": "Marion McMahon",
"userAvatar": "file:///android_asset/avatars/84.jpg",
"text": "🌳🍹🕌 Pulvinarinterpretaris erat persius at consequat omnesque qui erroribus cras odio suscipiantur consequat habitasse metus. Nisirhoncus decore fusce cu voluptatum reformidans pro honestatis utamur justo habitasse. Iaculisgubergren viderer affert pertinacia taciti proin graeco vero omittam necessitatibus appareat ludus dissentiunt fringilla ac verear. Impetusaptent detracto vehicula.⛲️📪\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374140,
"reply": null,
"poll": null
},
{
"id": "d4dac04c-9051-4e4c-a7a1-829816ff5b24",
"userNickname": "@platea",
"userFullName": "Phillip Gates",
"userAvatar": "file:///android_asset/avatars/137.jpg",
"text": "🔐🎐🕳\nProdicta expetendis dicunt verear mauris ponderum ligula inciderint mnesarchum duis deterruisset dico et netus interesset dictumst lacinia vehicula sapien. Delenitvoluptaria placerat conceptam definitiones quaestio tincidunt nascetur turpis mnesarchum. Malorumminim finibus comprehensam persequeris possim ut eum sit rhoncus tritani numquam consectetuer vulputate. Viverracurabitur neglegentur faucibus sociis sollicitudin pretium nibh voluptatum consul. Splendidemauris urna in noluisse est consectetuer litora purus similique convenire moderatius iudicabit te mucius. Requeadolescens sodales est hendrerit.\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374140,
"reply": null,
"poll": null
},
{
"id": "d2b3e991-e1e1-4e83-bfbe-ea9317fc2d27",
"userNickname": "@dicat",
"userFullName": "Dionne Sparks",
"userAvatar": "file:///android_asset/avatars/76.jpg",
"text": "👨💻🚍 consectetuer \n📎👻 💭🚎 debet \n🛁🍔 🐜🚗 habitasse \n👨👨👦🏤 🥨🥑 qui 🏗🗺 partiendo litora sodales \n🥂✂️ verterem ubique penatibus \n🍘🐄 🥠🍚 sumo \n⛩ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174140,
"reply": null,
"poll": null
},
{
"id": "af479216-0b7c-4020-802f-81f8f5d86b97",
"userNickname": "@conubi",
"userFullName": "Teddy Cameron",
"userAvatar": "file:///android_asset/avatars/121.jpg",
"text": "Necnatoque commodo deterruisset aliquam urbanitas gubergren vix quis ligula hac litora nisi quis massa vocent. Tinciduntfugit taciti luctus veniam nullam rutrum posidonium ultrices evertitur pulvinar suavitate laudem himenaeos.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374140,
"reply": null,
"poll": null
},
{
"id": "91fc76c7-878d-4283-b756-d4fdbaddf44f",
"userNickname": "@perpet",
"userFullName": "Weldon Lowe",
"userAvatar": "file:///android_asset/avatars/184.jpg",
"text": "varius a",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374140,
"reply": null,
"poll": null
},
{
"id": "ccd5af6a-98d3-4d05-a9dc-066be25cd99b",
"userNickname": "@prompt",
"userFullName": "Hung Freeman",
"userAvatar": "file:///android_asset/avatars/45.jpg",
"text": "contentiones doming",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174140,
"reply": null,
"poll": null
}
],
"createdAt": 1636983174140,
"reply": null,
"poll": null
},
{
"id": "0b502b83-9275-4a5c-b70f-bb0c2999ce16",
"userNickname": "@mus",
"userFullName": "Carl Wilcox",
"userAvatar": "file:///android_asset/avatars/129.jpg",
"text": "senserit has theophrastus 🏜 ",
"images": [],
"likes": 1,
"replyCount": 177,
"messages": 0,
"comments": [],
"createdAt": 1636939974140,
"reply": {
"id": "406524b9-afb4-46e6-a051-e6e7de730909",
"userNickname": "@inani",
"userFullName": "Tessa Wong",
"userAvatar": "file:///android_asset/avatars/169.jpg",
"text": "🖨 Conubiavivamus intellegebat detraxit pretium latine volumus novum hinc vituperata aenean. Nihildicta deserunt convenire nulla congue leo detraxit urna repudiandae mauris fuisset sagittis nulla melius quidam putent leo quidam. Adsenectus quaerendum voluptaria justo errem est contentiones molestie accusata iriure placerat dicunt felis. Melioredetracto explicari interpretaris quidam mea indoctum oratio periculis nostra nobis partiendo duis auctor aliquip fugit.🔏🚃 ",
"images": [],
"likes": 30,
"replyCount": 600,
"messages": 3,
"comments": [
{
"id": "1cbb7d1f-0fc0-490f-9ec1-05140c8fc7f9",
"userNickname": "@dicit",
"userFullName": "Lazaro Schroeder",
"userAvatar": "file:///android_asset/avatars/85.jpg",
"text": "🚀🚆 cetero 🏭 ✒️📟 antiopam \n📓 🥖👩🔧 legimus \n🐺💻 🍸🥜 mea 🚮👨👧 🎂🎐 dicam \n😜 🍷🍔 melius 🏤 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174140,
"reply": null,
"poll": null
},
{
"id": "d68fcaf3-e938-496b-a265-76f426b2f861",
"userNickname": "@vivend",
"userFullName": "Deena Chen",
"userAvatar": "file:///android_asset/avatars/14.jpg",
"text": "Attale iuvaret eget. Comprehensamnominavi regione nullam cetero consectetur no suas populo postea scelerisque luptatum. Elementumgraece recteque an vix.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774140,
"reply": null,
"poll": null
},
{
"id": "c6f9c7c0-e41a-4fd4-8712-798954875cfc",
"userNickname": "@imperd",
"userFullName": "Rosanne Hanson",
"userAvatar": "file:///android_asset/avatars/30.jpg",
"text": "tempus eos eam quaerendum enim",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174140,
"reply": null,
"poll": null
}
],
"createdAt": 1636929174140,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "33dfd376-73b7-40ba-af5f-ad72ce7f851c",
"userNickname": "@suscip",
"userFullName": "Cornelius Flowers",
"userAvatar": "file:///android_asset/avatars/102.jpg",
"text": "percipit animal detraxit evertitur",
"images": [],
"likes": 78,
"replyCount": 75,
"messages": 0,
"comments": [],
"createdAt": 1636997574140,
"reply": {
"id": "1e0d53c7-8775-4beb-afe9-4b20ea686b07",
"userNickname": "@metus",
"userFullName": "Melba Macias",
"userAvatar": "file:///android_asset/avatars/75.jpg",
"text": "pertinacia ponderum nostra",
"images": [],
"likes": 650,
"replyCount": 377,
"messages": 8,
"comments": [
{
"id": "87bcdeb5-f1f6-4172-8ddb-ae18a7b84b01",
"userNickname": "@rhoncu",
"userFullName": "Ned Everett",
"userAvatar": "file:///android_asset/avatars/122.jpg",
"text": "Comprehensamdetraxit reque ludus bibendum postulant. Consecteturposuere reprimique ridiculus tortor alia nunc necessitatibus. Arcuposuere pellentesque ornare alia inimicus sociosqu amet vix.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174140,
"reply": null,
"poll": null
},
{
"id": "f6f3ee88-9920-4967-97eb-9e84b7858de9",
"userNickname": "@ipsum",
"userFullName": "Tisha Burt",
"userAvatar": "file:///android_asset/avatars/116.jpg",
"text": "🍼🥂🖱 graecis💾 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574141,
"reply": null,
"poll": null
},
{
"id": "4f9030e7-c771-4739-a250-b4155042f6d6",
"userNickname": "@torque",
"userFullName": "Alfonso Vasquez",
"userAvatar": "file:///android_asset/avatars/51.jpg",
"text": "Pulvinarvis fermentum libero nostrum menandri suavitate dui altera posse dictum viderer meliore. Fusceridiculus dictum debet quas eos veri consetetur dolorum ante natoque verterem instructior homero a venenatis fames voluptatum sea. Percipitmaiorum a commune solet sea vitae quot gravida. Consectetuersodales taciti dicunt porta odio amet solum montes litora tractatos ullamcorper pharetra in diam. Orcisplendide accusata diam repudiare cras veri accommodare iriure persequeris commodo oratio elementum.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574141,
"reply": null,
"poll": null
},
{
"id": "8aa56329-85ea-47f2-a2dc-6cf5addacf33",
"userNickname": "@fugit",
"userFullName": "Geneva Montoya",
"userAvatar": "file:///android_asset/avatars/88.jpg",
"text": "recteque deseruisse dapibus \n🌆 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374141,
"reply": null,
"poll": null
},
{
"id": "32ba2a66-82e2-4d7c-b3b8-5e10d6d57cf3",
"userNickname": "@aliqui",
"userFullName": "Barney Carlson",
"userAvatar": "file:///android_asset/avatars/167.jpg",
"text": "📕\nduis ante indoctum ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774141,
"reply": null,
"poll": null
},
{
"id": "08ac4c53-939d-4e34-88bf-0f179595246c",
"userNickname": "@prompt",
"userFullName": "Everett Franks",
"userAvatar": "file:///android_asset/avatars/189.jpg",
"text": "aliquip alienum posidonium 🚫 🚐🐥 primis \n😎 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774141,
"reply": null,
"poll": null
},
{
"id": "60f10bfd-7073-491b-902a-97a330384557",
"userNickname": "@deseru",
"userFullName": "Sonny Gutierrez",
"userAvatar": "file:///android_asset/avatars/16.jpg",
"text": "numquam ultricies consetetur",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974141,
"reply": null,
"poll": null
},
{
"id": "a782f8de-ae8d-468c-8cd4-929e8bf7a93f",
"userNickname": "@omnesq",
"userFullName": "Dustin Roach",
"userAvatar": "file:///android_asset/avatars/193.jpg",
"text": "Egetunum laudem volumus diam vestibulum. Pertinaciaquas latine condimentum nonumes iisque adipiscing dolores gubergren per ludus senserit nisl porro vidisse fringilla imperdiet orci vero. Patrioquedonec mucius. Turpisquas maiestatis nostra purus dicant penatibus fabellas senectus blandit tincidunt lacus minim maiorum ancillae mi ius. Menandriverterem ex urna odio postea aliquet enim dui splendide eros accusata inceptos venenatis vel.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174141,
"reply": null,
"poll": null
}
],
"createdAt": 1636986774140,
"reply": null,
"poll": {
"isCompleted": false,
"totalVotes": 844,
"positions": [
{
"text": "maiorum agam intellegat porta ridiculus",
"voted": 245
},
{
"text": "odio vim docendi",
"voted": 201
},
{
"text": "commune similique mauris",
"voted": 398
}
]
}
},
"poll": null
},
{
"id": "882f0c17-c45b-4fdf-9fd3-cb1b4ca36ece",
"userNickname": "@sit",
"userFullName": "Carlene Jimenez",
"userAvatar": "file:///android_asset/avatars/198.jpg",
"text": "his delectus \n✒️ 🍧🎍 sollicitudin \n🌹 tractatos impetus curabitur \n📆 ‼️🔚 singulis \n🍨🤰 🎄▪️ qui \n🍧 adversarium quam audire 🛠🕶 pretium ponderum amet \n🔯 🍃🌲 vivendo \n🤘🐆 auctor mediocrem mediocrem 🚅🍳 quaeque constituam \n🦀 ",
"images": [
"file:///android_asset/post_images/70.jpg"
],
"likes": 632,
"replyCount": 642,
"messages": 0,
"comments": [],
"createdAt": 1637001174141,
"reply": null,
"poll": null
},
{
"id": "43649f12-5aae-4f2f-a69f-c025aa630208",
"userNickname": "@ultric",
"userFullName": "Erna Barker",
"userAvatar": "file:///android_asset/avatars/136.jpg",
"text": "dicat convenire ➰🍱 novum utroque malorum 🛋 🐻🍽 euripidis 😻 fabulas dignissim petentium \n💂♀ lobortis interpretaris 🏍 😟↘️ ultricies \n☘️ ",
"images": [
"file:///android_asset/post_images/80.jpg"
],
"likes": 779,
"replyCount": 31,
"messages": 7,
"comments": [
{
"id": "066907ab-7431-476e-8eff-b2ad6eb5c207",
"userNickname": "@volupt",
"userFullName": "Trina Torres",
"userAvatar": "file:///android_asset/avatars/108.jpg",
"text": "Promptahabemus dignissim mauris aliquet menandri ancillae lectus persius definitiones aenean menandri integer fringilla. Litoraconvallis offendit donec conclusionemque habitasse lorem vehicula vulputate nonumes esse enim ignota malorum.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574141,
"reply": null,
"poll": null
},
{
"id": "a2e468a3-261c-43cb-88a3-267f72e6fb3d",
"userNickname": "@natoqu",
"userFullName": "Reggie Ayala",
"userAvatar": "file:///android_asset/avatars/143.jpg",
"text": "Percipitsale contentiones quod petentium sed orci pulvinar vero adhuc volutpat oporteat facilis malesuada ancillae rhoncus vidisse sagittis. Suscipianturcu convenire maecenas propriae ne luptatum aenean veniam mandamus ubique invenire volumus placerat cetero reformidans causae. Vivendomolestie netus conubia iudicabit.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974141,
"reply": null,
"poll": null
},
{
"id": "7a07b862-ba30-4be1-b448-7f8237afc5ca",
"userNickname": "@alique",
"userFullName": "Lakeisha Acevedo",
"userAvatar": "file:///android_asset/avatars/22.jpg",
"text": "voluptatum discere singulis \n✅🚮 🍐🎑 pharetra \n🌎🖨 voluptatum nostra ridens \n🏯 🐠☦️ idque \n🍼 evertitur facilisis an \n📭 🙋♂🔵 fames ☀️🐠 definiebas inani 🈳 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974141,
"reply": null,
"poll": null
},
{
"id": "37f3c683-cd0c-44d9-b6b5-f36f9ec68a8b",
"userNickname": "@mei",
"userFullName": "Mandy Cash",
"userAvatar": "file:///android_asset/avatars/126.jpg",
"text": "🏔🖼 impetus \n🍸 nibh pericula assueverit 🕯🈺 urna phasellus \n📻✍️ 🍵🙈 brute \n🌿🎊 curabitur adversarium maximus \n👂 vitae tellus labores 🎶 🥝🐲 nullam \n🚤🍘 🐖🥥 intellegat \n🔌👢 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374141,
"reply": null,
"poll": null
},
{
"id": "04fe0558-32cc-4dac-b427-da11df8f15ad",
"userNickname": "@laudem",
"userFullName": "Zane Buck",
"userAvatar": "file:///android_asset/avatars/182.jpg",
"text": "Posteaquam ex natum. Nonumesinteger nostra impetus dui eirmod. Quemeius contentiones sit phasellus natum aperiri pretium possit lacus ne velit ludus noluisse curae referrentur. Torquentlatine constituam torquent netus atqui euripidis consectetur gloriatur falli vidisse torquent eruditi falli tractatos phasellus. Feugiatelectram consectetur constituto definitiones appareat.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774141,
"reply": null,
"poll": null
},
{
"id": "d5117f35-fd71-4346-b708-3b990914ebd1",
"userNickname": "@ligula",
"userFullName": "Guy Pope",
"userAvatar": "file:///android_asset/avatars/82.jpg",
"text": "🔨🍖♏️\nappetere platea proin graeco🥤\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174141,
"reply": null,
"poll": null
},
{
"id": "741f6bc1-af3a-4dfb-8747-44ab5945dec9",
"userNickname": "@possit",
"userFullName": "Sandy McConnell",
"userAvatar": "file:///android_asset/avatars/84.jpg",
"text": "📔 et ne minim🛬🍡\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574141,
"reply": null,
"poll": null
}
],
"createdAt": 1636979574141,
"reply": null,
"poll": null
},
{
"id": "d4ec2883-db79-4938-b1d0-7e2e52dc4c6b",
"userNickname": "@posuer",
"userFullName": "Santos Casey",
"userAvatar": "file:///android_asset/avatars/6.jpg",
"text": "💠🍿 gloriatur ⚖️ ",
"images": [],
"likes": 28,
"replyCount": 93,
"messages": 0,
"comments": [],
"createdAt": 1637011974141,
"reply": {
"id": "e53b1e35-a636-46ee-afdf-6718281847d1",
"userNickname": "@tale",
"userFullName": "Virgie Orr",
"userAvatar": "file:///android_asset/avatars/95.jpg",
"text": "📒🔓 tacimates \n🚶👩👧👦 sed curae \n📃🍴 😹🚿 fastidii 👡🚚 nihil ad ❗️🥐 mus natoque prodesset \n🦁🚥 aliquid fastidii mediocritatem 🐹 porro dictum parturient 🍜 ocurreret dolor brute 🍟⛑ 🖥🚜 putent 🦉🌻 ",
"images": [
"file:///android_asset/post_images/39.jpg",
"file:///android_asset/post_images/84.jpg"
],
"likes": 969,
"replyCount": 492,
"messages": 4,
"comments": [
{
"id": "1a30fa9f-1782-4788-a15f-35d12b14a356",
"userNickname": "@facili",
"userFullName": "Maritza Wade",
"userAvatar": "file:///android_asset/avatars/97.jpg",
"text": "▫️4️⃣ feugait \n🥨 🏍🎛 luptatum 👩🎓🎠 ornare sagittis 🦉 velit eros pharetra 🌌 🐇🥃 vituperata \n🍰🕤 dis viris theophrastus \n🌱 gubergren scripta dictumst \n😈 usu rutrum nisl 🖇 placerat vehicula fuisset \n🦇 🏷🗺 maximus \n💫👡 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774142,
"reply": null,
"poll": null
},
{
"id": "da81674d-99e0-4063-9ce8-9cad3e5d6873",
"userNickname": "@fames",
"userFullName": "Estelle Serrano",
"userAvatar": "file:///android_asset/avatars/98.jpg",
"text": "volumus",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974142,
"reply": null,
"poll": null
},
{
"id": "ae56d4e7-92db-4e95-a10c-d1c544cf0828",
"userNickname": "@iuvare",
"userFullName": "Ophelia Cain",
"userAvatar": "file:///android_asset/avatars/178.jpg",
"text": "movet nostra urna",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774142,
"reply": null,
"poll": null
},
{
"id": "1500eb3d-3a1b-48af-86bc-f6c9ca47b3d6",
"userNickname": "@ante",
"userFullName": "Claude Miles",
"userAvatar": "file:///android_asset/avatars/158.jpg",
"text": "Inan ligula quaerendum ornatus error epicuri. Efficiturconsetetur erat graecis. Vivendotation habitant iuvaret magna has nonumes aptent offendit pertinacia finibus lacinia inceptos agam. Delectusinteger vocibus verear fermentum nostra suavitate volutpat. Ultriciesmaecenas ponderum etiam doming singulis offendit audire elit comprehensam dolorem dicat nihil quisque.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174142,
"reply": null,
"poll": null
}
],
"createdAt": 1637011974141,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "9025145c-b0a3-4512-b945-9388214e2f5d",
"userNickname": "@ridicu",
"userFullName": "Lamar Dejesus",
"userAvatar": "file:///android_asset/avatars/41.jpg",
"text": "🍓🌭 voluptatum 🔍📩 ",
"images": [],
"likes": 32,
"replyCount": 183,
"messages": 0,
"comments": [],
"createdAt": 1636950774142,
"reply": {
"id": "ccb24402-503b-4b80-9311-9eb219c66036",
"userNickname": "@libris",
"userFullName": "Sue Kim",
"userAvatar": "file:///android_asset/avatars/45.jpg",
"text": "🚷🤳\nhac doctus aeque cu👩🚀\n",
"images": [],
"likes": 600,
"replyCount": 395,
"messages": 2,
"comments": [
{
"id": "02ebc5eb-37de-4048-ab73-f37072508835",
"userNickname": "@tristi",
"userFullName": "Lolita Buchanan",
"userAvatar": "file:///android_asset/avatars/63.jpg",
"text": "🦓🥫🚑 diam🛳\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974142,
"reply": null,
"poll": null
},
{
"id": "62992f59-2cef-4efc-bb43-27aa46763ef0",
"userNickname": "@massa",
"userFullName": "Bryce Pratt",
"userAvatar": "file:///android_asset/avatars/184.jpg",
"text": "feugait electram vitae",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574142,
"reply": null,
"poll": null
}
],
"createdAt": 1636943574142,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 818,
"positions": [
{
"text": "principes",
"voted": 22
},
{
"text": "erroribus solet",
"voted": 349
},
{
"text": "melius dicunt",
"voted": 292
},
{
"text": "torquent",
"voted": 155
}
]
}
},
"poll": null
},
{
"id": "04566780-7349-4744-8962-09b0e55581b1",
"userNickname": "@condim",
"userFullName": "Marquis Fernandez",
"userAvatar": "file:///android_asset/avatars/101.jpg",
"text": "Cubiliaatqui doming scripta deserunt petentium pertinacia eget suas dicat iriure an nullam taciti legimus omnesque mandamus. Nibhalienum ea referrentur. Mediocritatemalia sagittis ligula eius aliquid dicta.",
"images": [
"file:///android_asset/post_images/35.jpg",
"file:///android_asset/post_images/87.jpg"
],
"likes": 310,
"replyCount": 250,
"messages": 4,
"comments": [
{
"id": "e871ba39-be6f-4a66-a684-bb5046ac381e",
"userNickname": "@eripui",
"userFullName": "Antwan Rocha",
"userAvatar": "file:///android_asset/avatars/169.jpg",
"text": "intellegat integer",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574142,
"reply": null,
"poll": null
},
{
"id": "44ace42b-1ed3-4c57-9525-b52cf78ceca8",
"userNickname": "@erat",
"userFullName": "Dewayne Taylor",
"userAvatar": "file:///android_asset/avatars/147.jpg",
"text": "Tantasin tempus propriae sententiae metus ludus. Suasagam vel singulis tantas morbi vestibulum postulant elit egestas. Sollicitudindignissim definiebas class conubia idque labores tamquam ac mus. Definitionemequidem sodales luctus idque.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374142,
"reply": null,
"poll": null
},
{
"id": "88150b01-5086-4040-b148-17ff7e4e0b74",
"userNickname": "@tristi",
"userFullName": "Flossie Beach",
"userAvatar": "file:///android_asset/avatars/53.jpg",
"text": "debet posse doming \n🐉 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774142,
"reply": null,
"poll": null
},
{
"id": "903c2871-7de4-4624-aa7e-7b2425c11491",
"userNickname": "@platon",
"userFullName": "Faye Dickerson",
"userAvatar": "file:///android_asset/avatars/84.jpg",
"text": "🏬🌃\nubique populo audire falli😄\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774142,
"reply": null,
"poll": null
}
],
"createdAt": 1636993974142,
"reply": null,
"poll": null
},
{
"id": "ec6c461e-389a-4478-92b7-4da8a1c5abc8",
"userNickname": "@maiest",
"userFullName": "Fred Sherman",
"userAvatar": "file:///android_asset/avatars/55.jpg",
"text": "🙅♂ wisi consequat numquam affert altera🌶\n",
"images": [],
"likes": 71,
"replyCount": 18,
"messages": 0,
"comments": [],
"createdAt": 1636990374142,
"reply": {
"id": "e0c7fb71-3bc0-4c45-80cd-595af01bd0e9",
"userNickname": "@possit",
"userFullName": "Chelsea Ray",
"userAvatar": "file:///android_asset/avatars/90.jpg",
"text": "Rhoncusurbanitas mnesarchum vituperatoribus theophrastus. Principesverterem euripidis vel scelerisque errem tempus viris sapien vituperatoribus graeco graeco nostrum. Iaculisinimicus ei lectus augue invidunt hendrerit. Perpetuasapien euismod magna docendi. Fuscealiquid delenit maiorum aeque. Eosex sociosqu duo ex magnis contentiones meliore urbanitas utinam eu consectetur a recteque fermentum solet ut electram tortor luctus.",
"images": [
"file:///android_asset/post_images/52.jpg",
"file:///android_asset/post_images/83.jpg",
"file:///android_asset/post_images/67.jpg"
],
"likes": 635,
"replyCount": 369,
"messages": 4,
"comments": [
{
"id": "9301ac2b-0c24-4ed5-8df1-c727bab35b29",
"userNickname": "@posse",
"userFullName": "Darrin Pittman",
"userAvatar": "file:///android_asset/avatars/165.jpg",
"text": "Euismodreferrentur penatibus purus ridens utinam efficiantur ludus expetenda nulla deserunt definiebas animal semper vehicula brute disputationi graecis. Erremprimis cum meliore malorum ullamcorper maluisset fastidii rhoncus an. Tinciduntpostea deterruisset splendide nonumes penatibus senserit cetero tristique mauris affert sociosqu maluisset cu luptatum efficitur nihil alienum aenean. Mollismucius ancillae epicuri duo malesuada reque possit conclusionemque ferri dolore postea quod volumus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174142,
"reply": null,
"poll": null
},
{
"id": "402009de-c5ac-4626-bd54-c8d0e228e4db",
"userNickname": "@quaere",
"userFullName": "Mercedes Gutierrez",
"userAvatar": "file:///android_asset/avatars/26.jpg",
"text": "♐️ Deterruissetmontes vivendo iuvaret an risus liber aeque suscipit suscipit an quot elementum diam fugit noster tortor parturient imperdiet delectus. Moderatiusdis quaeque cetero lorem aliquam ancillae esse quod dissentiunt utroque affert alterum dolorum mi fusce possit vituperata consequat aliquid.🍬 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574142,
"reply": null,
"poll": null
},
{
"id": "f38ea4d3-9d1b-4e96-b357-912a99cef2d3",
"userNickname": "@offend",
"userFullName": "Ramiro Todd",
"userAvatar": "file:///android_asset/avatars/97.jpg",
"text": "🛣🥞 donec \n🐃 deterruisset nulla \n⛪️✖️ praesent quam 📸👽 eget fastidii fermentum \n🚇 morbi equidem putent 🎵 ㊗️⬆️ vivendo ☪️🥐 🅱️⛴ intellegat \n🖨 omittantur agam \n🥦🛒 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374142,
"reply": null,
"poll": null
},
{
"id": "69842a69-0ec6-4cab-ac16-8ca0810c62fd",
"userNickname": "@per",
"userFullName": "Edwardo Carver",
"userAvatar": "file:///android_asset/avatars/189.jpg",
"text": "repudiare te intellegat",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574142,
"reply": null,
"poll": null
}
],
"createdAt": 1636979574142,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "bca5332c-0f82-4bdb-b434-941fbe0841b5",
"userNickname": "@graeci",
"userFullName": "Laurence Prince",
"userAvatar": "file:///android_asset/avatars/26.jpg",
"text": "no",
"images": [],
"likes": 64,
"replyCount": 94,
"messages": 0,
"comments": [],
"createdAt": 1636936374143,
"reply": {
"id": "7cad5fe5-e364-4e42-af0c-8d8d712d3d0a",
"userNickname": "@legimu",
"userFullName": "Darrin Solis",
"userAvatar": "file:///android_asset/avatars/104.jpg",
"text": "Ubiquetristique ludus ad atqui expetendis consectetur tractatos velit fastidii aperiri. Sapientemsuspendisse feugait eget amet prodesset quot vulputate partiendo sententiae nec felis nihil curae reque cras eruditi potenti tacimates malorum. Doctusdoctus platea urna donec. Corrumpitconstituto contentiones cum natoque id contentiones ferri feugiat egestas quem netus scripta vis corrumpit auctor vehicula.",
"images": [
"file:///android_asset/post_images/18.jpg"
],
"likes": 225,
"replyCount": 248,
"messages": 1,
"comments": [
{
"id": "f6764021-8426-4557-87aa-be05fcc9e16d",
"userNickname": "@mnesar",
"userFullName": "Jesse Mendoza",
"userAvatar": "file:///android_asset/avatars/179.jpg",
"text": "Scelerisquevituperata prodesset interpretaris risus qualisque necessitatibus liber velit offendit has eos placerat. Commodofacilisi pericula solum facilis simul adolescens. Nullamutroque ultricies persecuti singulis repudiare similique dicam liber.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174143,
"reply": null,
"poll": null
}
],
"createdAt": 1636932774143,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "4be4ca2c-0112-4837-adae-c2f06bdb793a",
"userNickname": "@populo",
"userFullName": "Tania Battle",
"userAvatar": "file:///android_asset/avatars/55.jpg",
"text": "tempus feugiat aperiri docendi at",
"images": [],
"likes": 58,
"replyCount": 142,
"messages": 0,
"comments": [],
"createdAt": 1636957974143,
"reply": {
"id": "dbad831a-5fad-4b0c-8a78-5ce98bcf3dde",
"userNickname": "@effici",
"userFullName": "Guy Emerson",
"userAvatar": "file:///android_asset/avatars/144.jpg",
"text": "Eiusornare phasellus fabulas error dictas nisi minim montes malesuada netus doctus morbi nominavi suas mentitum epicuri. Tortoreuripidis tristique veri numquam ocurreret malesuada definitiones.",
"images": [],
"likes": 504,
"replyCount": 309,
"messages": 9,
"comments": [
{
"id": "41ba3916-9e59-4db5-8e72-68569a93444f",
"userNickname": "@vix",
"userFullName": "Marcy Cleveland",
"userAvatar": "file:///android_asset/avatars/8.jpg",
"text": "Efficiturquam felis. Suavitatecetero ante. Scriptaadipiscing offendit qualisque latine interpretaris. Fallidolor litora indoctum quisque autem reformidans iusto faucibus tota noster ad vis.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774143,
"reply": null,
"poll": null
},
{
"id": "21aa63e0-39aa-467e-86f6-beaa57a0e8c5",
"userNickname": "@accums",
"userFullName": "Anita Matthews",
"userAvatar": "file:///android_asset/avatars/41.jpg",
"text": "🔵🥣🐪 cum🥘🗳🚓 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774143,
"reply": null,
"poll": null
},
{
"id": "67ada7b5-d1b2-478c-8165-b7d5ff6ee69a",
"userNickname": "@class",
"userFullName": "Guy McPherson",
"userAvatar": "file:///android_asset/avatars/162.jpg",
"text": "🏤🐾👩👩👧👧\nquam perpetua🛎🚗 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774143,
"reply": null,
"poll": null
},
{
"id": "3a5679dc-a94a-4e9c-9c00-a25fb92bae84",
"userNickname": "@postul",
"userFullName": "Jimmie Miranda",
"userAvatar": "file:///android_asset/avatars/111.jpg",
"text": "arcu libris principes autem tritani",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374143,
"reply": null,
"poll": null
},
{
"id": "ea31d01c-0fc7-4358-a5cc-3fa8090e8a8e",
"userNickname": "@scrips",
"userFullName": "Angelia Wolf",
"userAvatar": "file:///android_asset/avatars/70.jpg",
"text": "Singulisfacilisis cras. Melioreposse vidisse urna iaculis facilisi autem leo voluptaria class mus justo prompta convenire turpis. Saperetconvenire quam nisi maximus. Consetetursem ei theophrastus omnesque enim epicuri curae graeci inani posse pulvinar has. Honestatisludus oratio curabitur qualisque enim cetero cetero habeo ferri sea elit adhuc viris.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574143,
"reply": null,
"poll": null
},
{
"id": "f0ffb859-d478-43fa-aaa7-92da88b65af2",
"userNickname": "@taciti",
"userFullName": "Francesca Mitchell",
"userAvatar": "file:///android_asset/avatars/75.jpg",
"text": "🗿\nConceptammagnis maluisset gravida facilisi dis vix autem sanctus consul. Pretiumea augue periculis nisl montes quas corrumpit himenaeos comprehensam consectetuer et gravida graeci utinam numquam liber eripuit persius. Plateaerroribus constituam constituto dicta ornatus tibique deterruisset quaestio singulis nihil. Perpetuavero nascetur quis. Fabulashendrerit detraxit volutpat animal facilis netus mattis in iaculis qui tempor sonet vix platea aperiri invenire.🌪 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974143,
"reply": null,
"poll": null
},
{
"id": "b165abda-a96c-45c7-9d6f-1b9749711acf",
"userNickname": "@suscip",
"userFullName": "Ed Young",
"userAvatar": "file:///android_asset/avatars/127.jpg",
"text": "persius conubia",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974143,
"reply": null,
"poll": null
},
{
"id": "0c134c8c-6a71-4b37-9e8b-08b07af706dd",
"userNickname": "@graeci",
"userFullName": "Alyssa Nelson",
"userAvatar": "file:///android_asset/avatars/28.jpg",
"text": "🚜🦄\nVelitcausae leo atqui nonumes errem reprimique pri sea his. Omittammagna blandit homero mollis graece taciti metus velit meliore porta vehicula hendrerit magnis lorem ferri convenire dictumst mandamus. Dicatdonec delicata aliquip. Pretiumei necessitatibus enim fabellas dissentiunt indoctum dictas natum interdum adversarium cras quod sea erroribus pri posuere vix accumsan. Iusad adversarium mutat corrumpit consequat nunc altera donec scripserit dui persecuti ius egestas gravida ornatus fugit porro. Quodcras pharetra ornare fuisset reprimique lacinia efficiantur vix consectetuer has decore suas ius molestiae iuvaret auctor duis sem.👩👧👦😷\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974143,
"reply": null,
"poll": null
},
{
"id": "cd8f54fc-87fc-4d4e-9f4b-4890a9810c1c",
"userNickname": "@alia",
"userFullName": "Louella Beck",
"userAvatar": "file:///android_asset/avatars/186.jpg",
"text": "reque vituperata sadipscing sed nullam",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174143,
"reply": null,
"poll": null
}
],
"createdAt": 1636957974143,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "c467f4e7-d927-4c3c-8b74-dc865c55287d",
"userNickname": "@accomm",
"userFullName": "Marisa Harrison",
"userAvatar": "file:///android_asset/avatars/164.jpg",
"text": "Auguelabores detraxit contentiones fabulas enim lorem alterum delenit pro persecuti. Vulputateinimicus interpretaris sit habeo ad option assueverit petentium maiorum consectetur porttitor no iaculis placerat nonumy. Euripidisaccusata utinam suscipit option commodo conubia vivendo cubilia mei imperdiet habitant tation conclusionemque vero sapientem. Minimaccumsan accusata suscipiantur. Tortorcivibus persecuti.",
"images": [],
"likes": 52,
"replyCount": 527,
"messages": 6,
"comments": [
{
"id": "0385c0dc-fb3b-44f9-a8d0-5738ba59b46f",
"userNickname": "@homero",
"userFullName": "Allison Gentry",
"userAvatar": "file:///android_asset/avatars/128.jpg",
"text": "Deterruissetgravida falli efficitur moderatius oporteat tamquam. Petentiumfeugiat sapientem hinc. Pertinaciaultrices salutatus suscipiantur euripidis aenean sem adipisci. Voluptariaconstituto percipit scelerisque neque praesent nisi splendide instructior possim menandri atqui intellegat sententiae dictumst noluisse adversarium. Sitfalli natum platea dico aliquip ante nominavi dui constituam.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374143,
"reply": null,
"poll": null
},
{
"id": "feb802c1-ce65-4207-8de1-bb01af56c159",
"userNickname": "@nostra",
"userFullName": "Lou McIntyre",
"userAvatar": "file:///android_asset/avatars/83.jpg",
"text": "🏣🌲 maiorum 👨👨👦 ✊🐜 hinc \n🎇🍔 per omittantur \n🕸 quidam magnis 🎍 💦🎇 inani \n🌗😷 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974143,
"reply": null,
"poll": null
},
{
"id": "52749d4b-1e0e-410e-bd71-347afa62dac0",
"userNickname": "@noster",
"userFullName": "Jolene Salinas",
"userAvatar": "file:///android_asset/avatars/145.jpg",
"text": "convenire antiopam postulant consul",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374143,
"reply": null,
"poll": null
},
{
"id": "fca41dd7-04d1-40d5-984a-001a73da2a96",
"userNickname": "@semper",
"userFullName": "Mervin Mills",
"userAvatar": "file:///android_asset/avatars/32.jpg",
"text": "🖥 Facilisicongue honestatis gloriatur adversarium meliore ludus malorum ancillae. Tellusduo pulvinar eam ultrices meliore invenire meliore. Sitsuscipit detraxit assueverit nascetur. Meineglegentur iudicabit petentium invidunt definitionem cubilia amet.\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974143,
"reply": null,
"poll": null
},
{
"id": "05fb1d92-ae9a-4907-8642-0ed8c1dd05aa",
"userNickname": "@eius",
"userFullName": "Joan Chang",
"userAvatar": "file:///android_asset/avatars/18.jpg",
"text": "👌🍱 fugit ⛰😬 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374143,
"reply": null,
"poll": null
},
{
"id": "c7567085-c54f-4069-be64-b6f8d8d8a0e3",
"userNickname": "@utinam",
"userFullName": "Luis Hines",
"userAvatar": "file:///android_asset/avatars/15.jpg",
"text": "🥃⏰ porttitor porro ubique🐷👈 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374143,
"reply": null,
"poll": null
}
],
"createdAt": 1636965174143,
"reply": null,
"poll": null
},
{
"id": "cb44d9f4-3698-4a50-89f1-f3027524c82e",
"userNickname": "@percip",
"userFullName": "Cynthia Macias",
"userAvatar": "file:///android_asset/avatars/155.jpg",
"text": "Habitasseviderer vim prodesset tortor saperet appetere commodo moderatius agam euismod sonet. Veniamalia qualisque mentitum mutat consequat urbanitas arcu nonumes. Doctuscivibus bibendum rutrum nihil. Nostrafinibus accommodare nascetur euismod indoctum principes error cursus pretium gloriatur cursus homero partiendo dico. Ligulaiudicabit assueverit volumus adipisci.",
"images": [],
"likes": 885,
"replyCount": 416,
"messages": 6,
"comments": [
{
"id": "6f2616c3-89bb-4454-8060-76ccd987432d",
"userNickname": "@antiop",
"userFullName": "Josephine Maxwell",
"userAvatar": "file:///android_asset/avatars/33.jpg",
"text": "appareat class equidem \n🥗🍛 🗻🏭 nam \n🍡🍅 🍤🐄 consequat 🐚 📷🌰 iuvaret \n🐑 partiendo tale sententiae \n🎶 ⬇️🏟 mnesarchum \n🚅↘️ ipsum a \n❓⏳ porttitor quaerendum affert 👡🛡 📁🕟 dicant 🗾🚁 urna quaeque consequat \n5️⃣ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374144,
"reply": null,
"poll": null
},
{
"id": "9865b65c-6017-442a-ae4b-5740f9956f4a",
"userNickname": "@nonume",
"userFullName": "Angelique Morgan",
"userAvatar": "file:///android_asset/avatars/42.jpg",
"text": "Utamurmovet sociosqu menandri congue menandri iusto nullam definitionem eius sententiae latine sapientem mattis elementum accommodare utroque. Quammenandri utamur maecenas non qui sodales delectus delenit reprehendunt explicari habeo sapientem ultrices. Suscipitdebet eirmod comprehensam suscipiantur esse dicant doming urna errem vel elit consetetur leo ad ignota quem impetus neque.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374144,
"reply": null,
"poll": null
},
{
"id": "1f333469-19aa-4599-ad93-1317235bc2c5",
"userNickname": "@vis",
"userFullName": "Polly Pacheco",
"userAvatar": "file:///android_asset/avatars/64.jpg",
"text": "♏️🕑 augue \n✴️ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374144,
"reply": null,
"poll": null
},
{
"id": "34fe26be-c4fd-4082-8017-fb9b181fda61",
"userNickname": "@percip",
"userFullName": "Ariel Fitzgerald",
"userAvatar": "file:///android_asset/avatars/193.jpg",
"text": "lacinia porttitor convenire non alia",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774144,
"reply": null,
"poll": null
},
{
"id": "5f4290ed-0a0d-4f49-9fe0-561f82c66a2f",
"userNickname": "@delica",
"userFullName": "Vicky Simon",
"userAvatar": "file:///android_asset/avatars/135.jpg",
"text": "tamquam cetero",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374144,
"reply": null,
"poll": null
},
{
"id": "105bbf98-a227-44c0-9d4c-34ad3aa63853",
"userNickname": "@nascet",
"userFullName": "Vonda Watson",
"userAvatar": "file:///android_asset/avatars/55.jpg",
"text": "🎥🗑🗝\nMeliusdoming postulant possit veri nonumy efficiantur accusata venenatis putent pharetra adipisci postulant offendit dicta ridens nostrum delenit. Etnihil noluisse simul inani per sale epicuri constituto pretium etiam ius solet graecis simul adipisci ipsum partiendo inceptos.🗿 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574144,
"reply": null,
"poll": null
}
],
"createdAt": 1636957974143,
"reply": null,
"poll": null
},
{
"id": "686043d1-b572-4ed3-8044-c2ad8a93a0c9",
"userNickname": "@est",
"userFullName": "Johnie Morris",
"userAvatar": "file:///android_asset/avatars/165.jpg",
"text": "regione convenire phasellus platea cu",
"images": [],
"likes": 75,
"replyCount": 45,
"messages": 0,
"comments": [],
"createdAt": 1636990374144,
"reply": {
"id": "4e3d12b8-a4c6-4f70-828e-820108e52aa9",
"userNickname": "@mei",
"userFullName": "Hector Dillon",
"userAvatar": "file:///android_asset/avatars/84.jpg",
"text": "Explicariintellegebat quis interesset atomorum saperet turpis viverra. Ultricesancillae velit dolorum splendide neglegentur definitiones velit faucibus. Voluptatibusorci accommodare principes phasellus eu fastidii aliquam eros doctus ligula consetetur tincidunt ancillae tota. Purusfeugiat dicunt vocent a lorem nisl facilisi ocurreret inani iuvaret senserit patrioque voluptatum mauris aliquet vituperatoribus equidem. Talecurabitur feugiat leo liber his aliquid sed sodales elementum labores aptent electram ocurreret epicurei volutpat sale sociosqu. Ancillaevitae harum vivendo altera.",
"images": [
"file:///android_asset/post_images/93.jpg",
"file:///android_asset/post_images/46.jpg",
"file:///android_asset/post_images/40.jpg",
"file:///android_asset/post_images/15.jpg",
"file:///android_asset/post_images/44.jpg"
],
"likes": 919,
"replyCount": 528,
"messages": 4,
"comments": [
{
"id": "5509e881-10a2-4f5e-bf15-c9fe1157854d",
"userNickname": "@nibh",
"userFullName": "Jordan Kline",
"userAvatar": "file:///android_asset/avatars/171.jpg",
"text": "sonet aliquet",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174144,
"reply": null,
"poll": null
},
{
"id": "bc0b9758-1b71-4fd3-8b84-3d66eb5f8c13",
"userNickname": "@facili",
"userFullName": "Javier Hutchinson",
"userAvatar": "file:///android_asset/avatars/80.jpg",
"text": "Nullasalutatus dicunt reprimique suscipiantur tacimates et eros felis ludus. Consulnihil porta senserit faucibus decore facilis ornare mandamus dolor suspendisse latine dicant justo eruditi non mentitum fugit nominavi moderatius. Nostrainvenire lorem nominavi saperet signiferumque. Pellentesquenon mei mediocritatem voluptatum quaerendum perpetua quaeque oporteat. Dicoac an enim interdum ligula iriure purus evertitur interesset.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374144,
"reply": null,
"poll": null
},
{
"id": "864ce00e-3bbc-4380-a833-cede87d47967",
"userNickname": "@utamur",
"userFullName": "Randolph Lyons",
"userAvatar": "file:///android_asset/avatars/42.jpg",
"text": "Movetmeliore morbi voluptaria. Menandrilatine nam fugit pertinax sit dis platea potenti epicuri rutrum cursus appareat aliquip ut corrumpit cu agam vis. Interpretarisaccumsan mattis definiebas taciti erat assueverit aliquid tritani. Conveniredelenit non volutpat magnis wisi autem quam maecenas detracto. Pulvinartempus offendit vivendo gloriatur habemus accusata civibus neglegentur phasellus. Dignissimdolor diam similique sem intellegebat docendi harum propriae convenire sed vis quod gravida.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174144,
"reply": null,
"poll": null
},
{
"id": "64f91889-bc7d-4d48-86ab-fff32d98e898",
"userNickname": "@antiop",
"userFullName": "Angelita Guerra",
"userAvatar": "file:///android_asset/avatars/16.jpg",
"text": "Volutpathabitasse postea leo molestiae iriure fusce. Namfacilisi ludus tristique quam. Sumoeget rhoncus molestie integer urbanitas solet non voluptatibus interpretaris nostrum malesuada potenti. Vestibulumplacerat dolores graeco propriae nominavi ornare falli augue utinam tantas. Ancillaeei adversarium morbi accommodare invenire errem noluisse utamur aliquid felis tellus putent cras vis feugait. Audiresuscipit eloquentiam referrentur et dictas adipiscing mattis saperet sem ut natum reque.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774144,
"reply": null,
"poll": null
}
],
"createdAt": 1636983174144,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "9588fbdb-3f6f-4552-b6c3-db2a2714cc21",
"userNickname": "@commod",
"userFullName": "Lesley Blair",
"userAvatar": "file:///android_asset/avatars/38.jpg",
"text": "verterem detraxit ultricies definitiones euismod",
"images": [],
"likes": 496,
"replyCount": 197,
"messages": 9,
"comments": [
{
"id": "ee540a80-0370-4500-863d-6869397c2c8a",
"userNickname": "@audire",
"userFullName": "Betty Fields",
"userAvatar": "file:///android_asset/avatars/186.jpg",
"text": "minim invenire morbi",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974144,
"reply": null,
"poll": null
},
{
"id": "95bc963e-24e3-4eae-ad90-578a9e4b3835",
"userNickname": "@eripui",
"userFullName": "Lauren Tran",
"userAvatar": "file:///android_asset/avatars/179.jpg",
"text": "Auguealiquip inceptos electram docendi iuvaret eu quas efficiantur cubilia oporteat morbi melius tempor parturient mi ante pertinacia suavitate consetetur. Adfabellas posuere tristique possim.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374144,
"reply": null,
"poll": null
},
{
"id": "f2ef55bd-b9ba-4f33-b1a7-766b8aebb12d",
"userNickname": "@massa",
"userFullName": "Pam Britt",
"userAvatar": "file:///android_asset/avatars/30.jpg",
"text": "Fabellassociosqu explicari appareat hendrerit eius option. Singulisdissentiunt neque aliquet delenit varius saperet doming platonem quam cu cras interdum aeque. Dicuntlaudem penatibus equidem repudiandae sapientem arcu facilisi congue gubergren himenaeos novum ante tale iudicabit hendrerit.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374144,
"reply": null,
"poll": null
},
{
"id": "016408d7-a3a6-460d-89b4-e452af292426",
"userNickname": "@dissen",
"userFullName": "Terence York",
"userAvatar": "file:///android_asset/avatars/110.jpg",
"text": "alia epicurei sonet \n⏱ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774144,
"reply": null,
"poll": null
},
{
"id": "3ab78c99-f778-4ae8-87d5-6ea77535bfeb",
"userNickname": "@hinc",
"userFullName": "Lilly Montgomery",
"userAvatar": "file:///android_asset/avatars/98.jpg",
"text": "Andonec vituperatoribus tortor principes tantas egestas aliquam cu lorem mei ponderum detracto laudem. Feugaitsaperet ridens sociis. Errortempus ne vero euripidis a latine aeque ultricies interpretaris postulant libero urbanitas voluptatum scripta qui adolescens dis vituperata. Omittanturmorbi quot morbi aliquam.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174144,
"reply": null,
"poll": null
},
{
"id": "9e6ce3ae-e50c-487f-ae7b-798867906a55",
"userNickname": "@legimu",
"userFullName": "Kelsey Bean",
"userAvatar": "file:///android_asset/avatars/188.jpg",
"text": "ac libris \n🐔 🚙🥕 omnesque \n🌆🦃 🏷⏳ brute \n✈️ 📤🗯 populo \n🏭🛵 🍢🛠 persecuti 🍍 🍐🚲 sodales 🎁🐠 sit praesent 👞 oratio conubia 🍞 purus nec 3️⃣ penatibus viverra 🥢🌕 🙎⚓️ gloriatur 😅 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974144,
"reply": null,
"poll": null
},
{
"id": "e4f5958c-2277-4327-a743-88fe03564090",
"userNickname": "@minim",
"userFullName": "Tina Davidson",
"userAvatar": "file:///android_asset/avatars/123.jpg",
"text": "🌉🐫\nnoster mel equidem0️⃣ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974144,
"reply": null,
"poll": null
},
{
"id": "ba5e6935-bd1c-44de-90c7-52e31dc3bdc6",
"userNickname": "@omitta",
"userFullName": "Latoya Blackburn",
"userAvatar": "file:///android_asset/avatars/132.jpg",
"text": "Erosnullam sodales graeci accumsan tritani posse at delenit netus repudiare consectetuer. Fermentumsonet expetenda simul tortor dignissim dignissim habitasse suas molestie vix pertinacia fusce mus. Detractodis eros ius habeo tincidunt ancillae habitant habemus vituperatoribus. Dignissimappareat habemus amet augue quaerendum ultrices pharetra mediocritatem sale eros pertinax explicari malorum civibus enim civibus repudiandae efficitur. Inaniamet lacinia sale potenti antiopam sea habitasse montes tempor iudicabit adversarium dictumst nihil no vituperatoribus leo.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174144,
"reply": null,
"poll": null
},
{
"id": "683fd21d-4c21-4f4e-89cc-c3b9457f5acf",
"userNickname": "@mucius",
"userFullName": "Tommy Hines",
"userAvatar": "file:///android_asset/avatars/35.jpg",
"text": "Sumomutat pharetra feugait iudicabit at mediocritatem definitionem malesuada pretium a justo ponderum brute magna nec pro iriure usu. Delectuscivibus cursus. Arcudolorem maluisset vel pertinacia vituperatoribus erat enim. Numquamusu percipit lobortis massa an tempus appetere.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174144,
"reply": null,
"poll": null
}
],
"createdAt": 1636990374144,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 1131,
"positions": [
{
"text": "ornare",
"voted": 383
},
{
"text": "dignissim consectetur voluptatibus facilis",
"voted": 411
},
{
"text": "taciti",
"voted": 54
},
{
"text": "eos erat",
"voted": 283
}
]
}
},
{
"id": "bf1b9642-3112-4895-9579-afa0bd8df96e",
"userNickname": "@euismo",
"userFullName": "Brendan Bray",
"userAvatar": "file:///android_asset/avatars/98.jpg",
"text": "🐶✅🍣\npercipit platea dolor\n",
"images": [],
"likes": 648,
"replyCount": 478,
"messages": 9,
"comments": [
{
"id": "f898662e-081f-458c-9ec3-c69f2377764e",
"userNickname": "@natoqu",
"userFullName": "Deidre Carlson",
"userAvatar": "file:///android_asset/avatars/77.jpg",
"text": "🎠\nEloquentiammauris facilis lacinia mediocrem epicurei meliore constituto electram alienum corrumpit recteque ubique decore assueverit mediocrem tantas. Nullacursus habemus noluisse honestatis vitae omittantur odio prodesset adolescens. Dolorumerroribus morbi mea indoctum dicit. Ususcripta iriure mucius vis repudiare volumus repudiare conceptam. ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374144,
"reply": null,
"poll": null
},
{
"id": "269b2885-1e39-43bd-a79d-1c6bf86a86a3",
"userNickname": "@vivend",
"userFullName": "Grady Francis",
"userAvatar": "file:///android_asset/avatars/81.jpg",
"text": "Repudiarepostea consequat saepe vehicula definiebas prodesset veniam his quot mi a varius lacinia sententiae viderer novum integer nisl. Platonemnoster neglegentur dolorem dui luptatum lacinia ubique in option scripserit expetendis aeque. Platonemcurae maluisset causae perpetua contentiones malorum adhuc melius. Aelaboraret doming pri quem verear tacimates luptatum epicuri. Gloriaturtantas disputationi agam elit numquam ludus splendide recteque eleifend vim recteque ferri mnesarchum inceptos legere iudicabit tota. Volumusnoster metus sapientem tristique repudiare brute natoque elitr graeci quis an.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574145,
"reply": null,
"poll": null
},
{
"id": "36a0969f-b7e0-40ee-bae8-9ca57a07e627",
"userNickname": "@region",
"userFullName": "Bradley Beard",
"userAvatar": "file:///android_asset/avatars/92.jpg",
"text": "🍾🛩\nrhoncus fabulas vulputate habeo ornatus🛩\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374145,
"reply": null,
"poll": null
},
{
"id": "66a9d5d6-d18a-4a5f-858a-dd0eb77f84fa",
"userNickname": "@aliqua",
"userFullName": "Sean Merrill",
"userAvatar": "file:///android_asset/avatars/99.jpg",
"text": "Odiovulputate donec faucibus postulant scripta suscipit eleifend dicta menandri nostrum viris vestibulum expetendis felis. Debetpostea indoctum postea usu conclusionemque ignota maiorum cras.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374145,
"reply": null,
"poll": null
},
{
"id": "2a5923ab-cf13-4687-bb72-803f9cab57f7",
"userNickname": "@deleni",
"userFullName": "Britney Casey",
"userAvatar": "file:///android_asset/avatars/93.jpg",
"text": "🖨🍨🥘\nHabitassereferrentur vel quaestio sociosqu mucius veritus tritani faucibus facilisis laudem mucius. Iudicabitquam suas aperiri scelerisque. Verearnominavi convenire definiebas ante tantas cetero imperdiet nonumes suscipiantur posse fames quas ad hinc luptatum veritus causae hinc. Viverradelectus dicant maximus. Ceterosnec metus enim aliquam leo adhuc natum explicari netus est agam tritani sociosqu adipisci persius ac.🥝\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974145,
"reply": null,
"poll": null
},
{
"id": "3804571c-8811-4592-81ee-a579856cc0a3",
"userNickname": "@amet",
"userFullName": "Tanisha Powers",
"userAvatar": "file:///android_asset/avatars/49.jpg",
"text": "nostrum accumsan",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774145,
"reply": null,
"poll": null
},
{
"id": "325c57d1-5ec9-4bfa-9582-5ad8ef81b754",
"userNickname": "@dapibu",
"userFullName": "Faye Suarez",
"userAvatar": "file:///android_asset/avatars/179.jpg",
"text": "doctus maluisset sed",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374145,
"reply": null,
"poll": null
},
{
"id": "486bb9bb-11d0-45df-8ed0-d5bf86de1ef7",
"userNickname": "@tellus",
"userFullName": "Caleb McFarland",
"userAvatar": "file:///android_asset/avatars/128.jpg",
"text": "Inimicussumo tellus dignissim sale dicit luptatum feugiat verear malorum accusata elaboraret maiorum vidisse suscipit congue theophrastus dui facilis. Montessententiae pri convenire per aliquip posse ludus nostrum potenti detraxit ei periculis partiendo oporteat egestas eleifend turpis ullamcorper. Natoquerecteque varius esse laudem decore eruditi fugit.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574145,
"reply": null,
"poll": null
},
{
"id": "6395eb0d-e838-483a-ba5f-7ff97eaf49b6",
"userNickname": "@tibiqu",
"userFullName": "Reinaldo Fuller",
"userAvatar": "file:///android_asset/avatars/149.jpg",
"text": "Ultriciesnibh fabellas euripidis. Pharetrapersequeris pharetra dictas mel suscipit dicit ceteros fringilla eget volumus. Menandrivitae a duis signiferumque discere risus dicant facilisi ancillae has per libris penatibus tation graece ubique nisl accusata. Repudiareperpetua libero mediocrem etiam corrumpit expetendis qualisque interpretaris magna dignissim orci magna autem definitionem definitiones.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774145,
"reply": null,
"poll": null
}
],
"createdAt": 1636968774144,
"reply": null,
"poll": {
"isCompleted": false,
"totalVotes": 825,
"positions": [
{
"text": "fusce",
"voted": 305
},
{
"text": "propriae faucibus vulputate tacimates",
"voted": 356
},
{
"text": "nisl nisl",
"voted": 118
},
{
"text": "meliore",
"voted": 46
}
]
}
},
{
"id": "a15b5491-b0c4-4bbe-a615-3fbbd226aeeb",
"userNickname": "@mel",
"userFullName": "Althea McPherson",
"userAvatar": "file:///android_asset/avatars/23.jpg",
"text": "vulputate sumo causae",
"images": [],
"likes": 79,
"replyCount": 145,
"messages": 0,
"comments": [],
"createdAt": 1636961574145,
"reply": {
"id": "49191a46-b973-4164-95ef-9e29f764fc51",
"userNickname": "@vocibu",
"userFullName": "Jessica Morrow",
"userAvatar": "file:///android_asset/avatars/194.jpg",
"text": "🌲🐥 lacinia ♥️ 🚽🌠 gloriatur \n🏜 🦒🤤 suscipiantur 🍬 🐲🌂 sonet 💬 🥦🦊 vim 😊 ",
"images": [],
"likes": 585,
"replyCount": 564,
"messages": 0,
"comments": [],
"createdAt": 1636961574145,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "3c419e86-f8c8-4e43-b68a-156539d6f878",
"userNickname": "@aptent",
"userFullName": "Christy Vargas",
"userAvatar": "file:///android_asset/avatars/142.jpg",
"text": "facilisis adipiscing deseruisse 🐢 ",
"images": [],
"likes": 59,
"replyCount": 69,
"messages": 0,
"comments": [],
"createdAt": 1636943574145,
"reply": {
"id": "b1bb94de-8b27-4ebb-8ec3-1544183547d1",
"userNickname": "@quis",
"userFullName": "Maryann Contreras",
"userAvatar": "file:///android_asset/avatars/75.jpg",
"text": "magna ad invidunt \n🥗 👨👨👧👦✉️ ponderum 🗯 ",
"images": [
"file:///android_asset/post_images/36.jpg"
],
"likes": 254,
"replyCount": 497,
"messages": 3,
"comments": [
{
"id": "96ce0fc3-a443-4138-910a-cd5d099c0541",
"userNickname": "@adipis",
"userFullName": "Bradford Taylor",
"userAvatar": "file:///android_asset/avatars/134.jpg",
"text": "🍟😳\nmeliore qualisque verear penatibus libero🎄🔠\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974145,
"reply": null,
"poll": null
},
{
"id": "054bd3d9-dd5f-4c4f-84e8-dda9d067bcc1",
"userNickname": "@indoct",
"userFullName": "Osvaldo Sampson",
"userAvatar": "file:///android_asset/avatars/81.jpg",
"text": "Tritanifuisset eget mazim hendrerit mauris nihil tale graeco similique vituperata electram urna. Adversariumnumquam odio placerat quidam splendide voluptatibus te noluisse maluisset platea neque placerat dicta dicunt nulla. Auctornostrum esse. Placeratnon his affert similique aenean hinc labores eleifend dolorum animal ceteros inimicus neglegentur affert dolore signiferumque graecis. Hincquas euismod antiopam inimicus himenaeos adhuc consectetur has reformidans audire habemus quaestio omittam justo. Detraxittantas at ut feugait assueverit tibique feugait at sed iudicabit.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774145,
"reply": null,
"poll": null
},
{
"id": "4393d8fa-55f1-4df8-98db-558d228f8238",
"userNickname": "@postea",
"userFullName": "Kyle Atkinson",
"userAvatar": "file:///android_asset/avatars/131.jpg",
"text": "👳📊 iudicabit ♣️💜 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174145,
"reply": null,
"poll": null
}
],
"createdAt": 1636929174145,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "a506fde2-9be3-47f0-930e-537ab3b1fa2e",
"userNickname": "@region",
"userFullName": "Lorraine Brock",
"userAvatar": "file:///android_asset/avatars/30.jpg",
"text": "Suscipianturanimal luctus egestas cum justo suscipit discere maximus necessitatibus fames malesuada disputationi mediocrem intellegebat habemus efficiantur numquam ornare. Sociosqugloriatur liber numquam sonet mei fringilla sem libero convenire porro molestiae reprimique eius wisi.",
"images": [],
"likes": 733,
"replyCount": 256,
"messages": 7,
"comments": [
{
"id": "d7864d53-bd63-4d28-ac64-8d5d49baf6b4",
"userNickname": "@venena",
"userFullName": "Juliet Silva",
"userAvatar": "file:///android_asset/avatars/27.jpg",
"text": "🈸🌟🍕\nMeliusprimis pretium. Dissentiunterrem interpretaris mel minim maiestatis regione. Inciderintadipisci option. Novumconstituto appareat cras id maluisset senserit invenire pri menandri pulvinar elaboraret mi. Duilabores vidisse eget platea evertitur movet luptatum molestiae. ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374145,
"reply": null,
"poll": null
},
{
"id": "1aedd383-da60-4a66-8cc9-745303badb02",
"userNickname": "@eam",
"userFullName": "Kyle Henderson",
"userAvatar": "file:///android_asset/avatars/101.jpg",
"text": "🥥🖕🏗 Euismodeirmod pertinax. Dissentiuntvehicula habitant sadipscing his mel parturient dicam feugiat turpis maiorum cum maecenas pertinax movet ultrices. Delenitfacilis veniam urbanitas atomorum urbanitas justo. Similiqueinvidunt ornatus utamur moderatius mentitum mel reque honestatis sit similique velit eloquentiam. Integerhomero dicunt corrumpit no viderer purus alia delenit dui consetetur electram pertinacia elementum ei delicata impetus eius ex.🍂🐨🔹 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574145,
"reply": null,
"poll": null
},
{
"id": "51383a48-9a01-40cd-b697-616e5849aafa",
"userNickname": "@volupt",
"userFullName": "Howard Rodriguez",
"userAvatar": "file:///android_asset/avatars/69.jpg",
"text": "🏩🦔🌁\nTractatosperpetua vidisse verterem salutatus erroribus. Eratporro libris utroque fermentum graecis. Curabiturcursus possit detraxit laudem donec congue elaboraret persecuti reprimique quaerendum curabitur impetus arcu cras deserunt prompta eruditi tortor.🛰📁\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174145,
"reply": null,
"poll": null
},
{
"id": "41a31daa-2c19-49db-8bef-ac9f72e54c12",
"userNickname": "@dapibu",
"userFullName": "Cathryn Perez",
"userAvatar": "file:///android_asset/avatars/179.jpg",
"text": "veri debet malorum",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974145,
"reply": null,
"poll": null
},
{
"id": "f4edc6a4-7df4-401a-9fb3-8056666e4724",
"userNickname": "@senect",
"userFullName": "Darla Kemp",
"userAvatar": "file:///android_asset/avatars/37.jpg",
"text": "Cumappareat postea adversarium dicam congue fabellas mei. Nominavidapibus intellegat sapien duis mi pericula latine egestas natum decore. Instructiorimpetus melius eum. Eruditisadipscing facilisis vocibus enim id deterruisset prodesset arcu quas platea intellegebat sagittis dictumst consequat ac debet quo enim. Vitaetincidunt finibus etiam vivamus solum cum id tempus malorum sociis volutpat natoque vehicula donec cubilia.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574145,
"reply": null,
"poll": null
},
{
"id": "269ad006-7194-4df7-9b9c-8131f8e0fcb1",
"userNickname": "@quaere",
"userFullName": "Johnny Carey",
"userAvatar": "file:///android_asset/avatars/174.jpg",
"text": "📄🆚🍨 Sententiaefacilisis wisi intellegebat accumsan nihil eros inimicus. Nepersecuti volumus. Nequeposse aperiri definiebas iuvaret vehicula omittantur sententiae graeci. Aliquetcum saepe alienum periculis quem dictum brute varius deseruisse sapientem suas vitae auctor quot solet conclusionemque. Ignotafringilla petentium sociosqu veniam. Consulprompta periculis facilisi primis delenit odio proin petentium varius nominavi affert.😪 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174145,
"reply": null,
"poll": null
},
{
"id": "37e4f012-a31b-4200-9370-841e26b4e800",
"userNickname": "@mandam",
"userFullName": "Morgan Ellison",
"userAvatar": "file:///android_asset/avatars/26.jpg",
"text": "mediocritatem meliore \n🏕 💫🛷 sem ◽️ 🐟🤖 cetero \n🙇 intellegebat has ullamcorper \n🍛 vidisse facilisi ubique 🦋🌙 blandit offendit luptatum \n🐚 dis referrentur erroribus 🚆 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774145,
"reply": null,
"poll": null
}
],
"createdAt": 1636950774145,
"reply": null,
"poll": null
},
{
"id": "f3b5400b-6ed5-4bb9-9e7f-270b3a9e9e43",
"userNickname": "@consec",
"userFullName": "Brandy Valenzuela",
"userAvatar": "file:///android_asset/avatars/132.jpg",
"text": "mauris",
"images": [],
"likes": 20,
"replyCount": 187,
"messages": 0,
"comments": [],
"createdAt": 1636957974146,
"reply": {
"id": "164e8613-21d0-4388-a182-a1817812cc97",
"userNickname": "@labore",
"userFullName": "Tanisha Petty",
"userAvatar": "file:///android_asset/avatars/106.jpg",
"text": "Famesat ullamcorper turpis meliore meliore fastidii tractatos errem quis fames elit suscipiantur. Vituperatadis expetenda sapientem neque congue nonumy platonem varius netus vivendo nascetur risus. Intellegatconsectetuer cum alienum praesent magnis mutat conclusionemque sed sea disputationi. Instructioreuismod nostrum habitant sollicitudin viverra lacus sollicitudin tritani dapibus suspendisse unum sonet interpretaris invenire natoque ultricies mus.",
"images": [],
"likes": 165,
"replyCount": 563,
"messages": 2,
"comments": [
{
"id": "d93f1576-75ff-4ca3-a304-44bb881852a0",
"userNickname": "@script",
"userFullName": "Kaye Whitaker",
"userAvatar": "file:///android_asset/avatars/94.jpg",
"text": "⛎\nCurabiturvarius vocibus numquam quaestio nec cu tibique maiestatis ubique inciderint cu euripidis. Sumodocendi esse molestie eius ancillae honestatis porta cras ex persequeris voluptaria posse ocurreret euismod utroque. Dicuntvivamus dolorem expetenda ne mauris ignota turpis natoque eirmod fermentum veritus. Possimqualisque errem accommodare laoreet eirmod et senectus. Graecohabemus natum disputationi similique neque metus habitant alienum dis vehicula constituto lectus discere nullam enim viverra. Possitutinam nec evertitur turpis pretium netus natoque perpetua utamur vulputate class altera et doctus altera feugait consul aptent contentiones.ℹ️\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574146,
"reply": null,
"poll": null
},
{
"id": "43af595c-0c0d-414d-905a-be9a7b225f79",
"userNickname": "@falli",
"userFullName": "Cassandra Rodriguez",
"userAvatar": "file:///android_asset/avatars/196.jpg",
"text": "libero quas singulis posse",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574146,
"reply": null,
"poll": null
}
],
"createdAt": 1636943574146,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "0bd2aa15-46e0-4cf1-9ddd-6dd6a9b5973a",
"userNickname": "@doctus",
"userFullName": "Duncan Foster",
"userAvatar": "file:///android_asset/avatars/115.jpg",
"text": "🍅🌩🍵 liber platonem\n",
"images": [],
"likes": 44,
"replyCount": 26,
"messages": 0,
"comments": [],
"createdAt": 1636990374146,
"reply": {
"id": "264c773e-ba1f-469d-9fbb-b2fa90cd3699",
"userNickname": "@electr",
"userFullName": "Alberto Glover",
"userAvatar": "file:///android_asset/avatars/107.jpg",
"text": "🔦🐻🚈 Felisiusto vituperata sociosqu fringilla class mel gravida amet meliore commune lobortis meliore cras vocibus oratio adolescens reprehendunt tempus pretium. Vidissedictas aeque assueverit nulla corrumpit feugait lacinia. Litoraea reprehendunt ea invenire civibus vocibus libero dicit dicant an vitae vulputate brute sonet iisque definiebas feugiat penatibus adhuc. Appareatmetus sagittis cetero saepe dolorum solum vidisse sale quot. Risusmassa ludus omittantur debet ponderum feugait persius volumus sodales. Elitdeterruisset accusata delicata ornare reque honestatis bibendum tibique conceptam dicunt amet dolorum convallis.🍲🦓🎥\n",
"images": [
"file:///android_asset/post_images/98.jpg",
"file:///android_asset/post_images/23.jpg"
],
"likes": 612,
"replyCount": 48,
"messages": 0,
"comments": [],
"createdAt": 1636975974146,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "79abd8d2-5c05-494e-b82c-2c328ae4124e",
"userNickname": "@scrips",
"userFullName": "Parker Pope",
"userAvatar": "file:///android_asset/avatars/90.jpg",
"text": "nascetur",
"images": [],
"likes": 28,
"replyCount": 78,
"messages": 0,
"comments": [],
"createdAt": 1636990374146,
"reply": {
"id": "88bde939-5a2f-4209-8ad1-4aa1faa011a9",
"userNickname": "@viris",
"userFullName": "Ruben Finley",
"userAvatar": "file:///android_asset/avatars/28.jpg",
"text": "🍭🐞 Nosteraltera vocent placerat error malorum iuvaret ne qualisque iuvaret ullamcorper possit. Commodoalterum quaerendum interesset maluisset torquent dolore eius harum an pericula suscipit. Detractodetracto graeci. Tortorinceptos offendit blandit epicuri nostrum ultrices magna convallis te quo porttitor nibh contentiones taciti quaerendum legimus invenire. Necessitatibusconclusionemque phasellus odio. Soletdolorum referrentur definiebas oporteat sententiae purus mandamus maecenas gravida euripidis veritus repudiandae appetere.📈🛋🛴\n",
"images": [],
"likes": 447,
"replyCount": 691,
"messages": 5,
"comments": [
{
"id": "9564f505-0c1b-4062-bcb6-d77b40d28b24",
"userNickname": "@cetero",
"userFullName": "Ilene De La Cruz",
"userAvatar": "file:///android_asset/avatars/123.jpg",
"text": "Eratrepudiare risus suscipiantur euismod senectus definitionem class dicit molestiae impetus fastidii eros maiestatis quot omnesque atqui. Eloquentiamauctor adipisci graece idque sociis docendi nulla iudicabit delenit tibique esse. Posuereexplicari eu curae autem definiebas.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174146,
"reply": null,
"poll": null
},
{
"id": "8b758d19-ad34-4419-97a2-c1b3f8aee53a",
"userNickname": "@cras",
"userFullName": "Letitia Sampson",
"userAvatar": "file:///android_asset/avatars/20.jpg",
"text": "Nooporteat maximus amet ponderum. Habeomagnis nunc ridens commodo sapientem signiferumque habeo habitant eleifend. Saperetnovum vivamus dictum ornatus aeque delectus lacinia tibique non. Detraxitmea tacimates auctor. Ametmovet sapientem assueverit. Saepeluptatum elitr appareat inciderint eam nihil pertinax partiendo vituperata comprehensam tota eos justo eius neque odio eripuit netus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374146,
"reply": null,
"poll": null
},
{
"id": "f6140969-e0fb-458e-842a-5eb053096b94",
"userNickname": "@senect",
"userFullName": "Clifton Dickerson",
"userAvatar": "file:///android_asset/avatars/79.jpg",
"text": "molestiae molestiae quidam deseruisse deseruisse",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774146,
"reply": null,
"poll": null
},
{
"id": "98e28418-51d0-4bc7-9688-15a6f88edf80",
"userNickname": "@agam",
"userFullName": "Rusty Dennis",
"userAvatar": "file:///android_asset/avatars/146.jpg",
"text": "Nuncex nulla tibique dicunt cetero quem errem viverra persius explicari ocurreret mentitum atqui altera pretium. Nostracu curabitur vulputate turpis tempor persecuti. Conguetempus quas penatibus habitant quidam dictum nostrum rhoncus eget accumsan dicant lobortis graeci scripta docendi moderatius tale tamquam.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974146,
"reply": null,
"poll": null
},
{
"id": "f748a68d-7182-4edf-a2e0-4deaf538b628",
"userNickname": "@possit",
"userFullName": "Norris Winters",
"userAvatar": "file:///android_asset/avatars/65.jpg",
"text": "Intellegatintellegebat noster habitasse definiebas neglegentur et audire dico massa habitasse vulputate saperet inimicus splendide ornatus tempor quo novum. Dicuntvituperata minim affert cras litora convenire wisi mattis fugit blandit dis. Fermentumcondimentum minim reformidans omittantur duis nonumes lacinia equidem conceptam definitiones dicit urna omittam. Sedmauris habitasse expetendis inciderint vivendo mea habemus ligula purus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374146,
"reply": null,
"poll": null
}
],
"createdAt": 1636975974146,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "ed84dcbd-4e21-44c3-bd80-5d1f393819a4",
"userNickname": "@nonume",
"userFullName": "Reynaldo Morse",
"userAvatar": "file:///android_asset/avatars/17.jpg",
"text": "Bibendumreprimique inani duo ac fuisset minim est. Repudiaretempor docendi id nostrum sodales suavitate interpretaris consectetur iusto ancillae te vituperatoribus numquam sed nominavi gubergren varius.",
"images": [
"file:///android_asset/post_images/69.jpg",
"file:///android_asset/post_images/64.jpg",
"file:///android_asset/post_images/78.jpg"
],
"likes": 372,
"replyCount": 7,
"messages": 7,
"comments": [
{
"id": "2fffdf2a-3724-4d52-aa79-c8421dbc2525",
"userNickname": "@rhoncu",
"userFullName": "Nona Petersen",
"userAvatar": "file:///android_asset/avatars/185.jpg",
"text": "justo pulvinar 📥🛒 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774146,
"reply": null,
"poll": null
},
{
"id": "ef1b0f92-0d57-496f-a6b0-55f36ae1c9aa",
"userNickname": "@urbani",
"userFullName": "Yvette Trevino",
"userAvatar": "file:///android_asset/avatars/77.jpg",
"text": "mel",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774146,
"reply": null,
"poll": null
},
{
"id": "39dbc21b-d27b-4c40-92cb-8e6a388bc310",
"userNickname": "@nullam",
"userFullName": "Janet Wagner",
"userAvatar": "file:///android_asset/avatars/32.jpg",
"text": "orci mi 🔎 etiam elaboraret \n🚋🍽 duo litora \n♌️🍌 nulla invidunt verear \n🔇 adhuc vestibulum invenire \n⏲ erat omittam 🍐 cubilia habeo ☔️ detraxit fames \n🏃♀ regione eruditi latine 🦎 🐄🎆 detraxit 👩🌾🥐 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774147,
"reply": null,
"poll": null
},
{
"id": "e21adb94-d7fe-4c2f-9996-ded7e3e9c3be",
"userNickname": "@dicat",
"userFullName": "Phoebe Singleton",
"userAvatar": "file:///android_asset/avatars/0.jpg",
"text": "🐭 Aliaplatea nihil ullamcorper definiebas. Dapibuseu salutatus pertinax suscipiantur mei fames. Maurisqui doming altera dolor dapibus id.📿🕑👨🎓 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974147,
"reply": null,
"poll": null
},
{
"id": "c56a098b-7e8c-41fa-987f-d8b56e0d9412",
"userNickname": "@omitta",
"userFullName": "Sammy Mills",
"userAvatar": "file:///android_asset/avatars/98.jpg",
"text": "convenire natum risus",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374147,
"reply": null,
"poll": null
},
{
"id": "2d4f8f5f-ca6c-46ae-a6f5-e6e92fae20c5",
"userNickname": "@himena",
"userFullName": "Nestor Bentley",
"userAvatar": "file:///android_asset/avatars/86.jpg",
"text": "🌑🛒 neglegentur \n🍱 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574147,
"reply": null,
"poll": null
},
{
"id": "3e370033-974e-4586-8401-70412c3cdb95",
"userNickname": "@dicant",
"userFullName": "Colleen Shields",
"userAvatar": "file:///android_asset/avatars/20.jpg",
"text": "pretium discere ligula",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374147,
"reply": null,
"poll": null
}
],
"createdAt": 1636947174146,
"reply": null,
"poll": null
},
{
"id": "ec33a467-4dcd-4219-9871-a34e43992ea8",
"userNickname": "@simul",
"userFullName": "Bernardo Snow",
"userAvatar": "file:///android_asset/avatars/105.jpg",
"text": "Ultricesmattis facilisi. Novuminterpretaris duo lectus tota suas platea invidunt instructior sollicitudin ut affert nibh. Graecoerat suspendisse. Optiontincidunt vocibus ipsum dolore impetus reprimique dolorem nihil cum putent penatibus quot platonem quisque justo. Tacitisingulis fabellas detracto porttitor postulant conubia fabellas persecuti venenatis splendide aperiri ferri felis suscipit brute iisque.",
"images": [],
"likes": 583,
"replyCount": 598,
"messages": 7,
"comments": [
{
"id": "56c4ca69-9a3c-4adb-a3fd-b9eb5dc6e60a",
"userNickname": "@effici",
"userFullName": "Winfred Rosales",
"userAvatar": "file:///android_asset/avatars/152.jpg",
"text": "🦖🏝 Cudolore equidem eripuit eruditi adipisci et eleifend an expetenda nisl homero. Adversariumnibh pharetra torquent. Veritusunum tractatos accumsan leo reque reformidans tantas nibh ubique sadipscing diam. Maximussale harum persequeris error lacinia tellus suscipit altera equidem mutat idque explicari possim neque doctus delenit nonumy. Iriurehabitasse voluptaria mediocrem ignota euripidis simul senectus vix decore. Turpisconubia ullamcorper saepe.🐣🈂️📽\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974147,
"reply": null,
"poll": null
},
{
"id": "e4f1e968-ca0e-4c62-8362-287d321bcff3",
"userNickname": "@sollic",
"userFullName": "Dewayne Holder",
"userAvatar": "file:///android_asset/avatars/51.jpg",
"text": "🎍👆8️⃣\nVivendoregione justo eos doming urbanitas molestiae an minim habeo iusto dictum dignissim pretium nobis nam eruditi no. Verolaoreet his laoreet habemus regione inani. Dignissimefficiantur lacus bibendum hinc debet dicat class equidem taciti referrentur placerat etiam torquent nostra animal convenire potenti. Persecutidictumst latine detraxit fuisset iuvaret explicari molestie invenire nisl erat. Eiusiusto constituam aliquid melius scripta mutat alia omittantur adhuc ac honestatis finibus alterum montes expetenda.♣️🚔🌩 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574147,
"reply": null,
"poll": null
},
{
"id": "c234f733-2434-4b43-a142-feafc412e00b",
"userNickname": "@graeco",
"userFullName": "Bennie Short",
"userAvatar": "file:///android_asset/avatars/177.jpg",
"text": "🌮😕\npharetra tractatos iisque pertinacia aeque🕖🍷\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374147,
"reply": null,
"poll": null
},
{
"id": "c4f05fbc-9c67-4d18-90d2-d78bac969cc4",
"userNickname": "@morbi",
"userFullName": "Tricia Landry",
"userAvatar": "file:///android_asset/avatars/174.jpg",
"text": "📘😋\nDomingdeserunt deterruisset vero equidem parturient persequeris ridiculus voluptatibus inciderint detraxit graece tincidunt fugit graecis delenit. Requeadhuc neglegentur sollicitudin lacinia orci graeco ius nascetur volumus montes ridiculus vim vituperatoribus eam suscipiantur mauris. Persecutivoluptaria veniam ridiculus facilis audire suas suscipit euismod impetus posidonium potenti elitr mucius a lacus eirmod.🗺🥔🐗\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574147,
"reply": null,
"poll": null
},
{
"id": "74b0b7e2-1adb-4df5-8333-9a64baf737e8",
"userNickname": "@utroqu",
"userFullName": "Phil Glass",
"userAvatar": "file:///android_asset/avatars/17.jpg",
"text": "aliquet maiestatis ceteros sea",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574147,
"reply": null,
"poll": null
},
{
"id": "d5982b7b-0abb-4ee6-b44f-12477b08de4f",
"userNickname": "@deleni",
"userFullName": "Major Vazquez",
"userAvatar": "file:///android_asset/avatars/53.jpg",
"text": "🍱👨👨👦 cum \n🗳⬅️ a quaestio 🌕 🚹📮 pericula 🙃🍅 🈹🍛 possim \n🥫 🎥🐞 malesuada \n🍁 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374147,
"reply": null,
"poll": null
},
{
"id": "b5b70eb9-4a29-4647-8ff8-193e271825b5",
"userNickname": "@decore",
"userFullName": "Colette Rutledge",
"userAvatar": "file:///android_asset/avatars/19.jpg",
"text": "🐡🏣🍾 Mollisaccusata ceteros alia reformidans lacinia his maecenas voluptatibus class pretium pulvinar pertinacia conclusionemque. Auguealia cu melius dicta prompta enim delicata movet ante populo nisl posuere dolorem rutrum ridiculus dui consetetur pretium nullam.💋🙍🍇\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574147,
"reply": null,
"poll": null
}
],
"createdAt": 1636979574147,
"reply": null,
"poll": null
},
{
"id": "5a808b4e-ea77-42ef-84c4-486406b5ca4d",
"userNickname": "@omitta",
"userFullName": "Kyle Pate",
"userAvatar": "file:///android_asset/avatars/93.jpg",
"text": "ubique reprehendunt omnesque neque",
"images": [],
"likes": 79,
"replyCount": 60,
"messages": 0,
"comments": [],
"createdAt": 1636979574147,
"reply": {
"id": "6a2e40bf-18c5-40d8-ba58-a50f4394dce6",
"userNickname": "@vestib",
"userFullName": "Greg Vaughn",
"userAvatar": "file:///android_asset/avatars/158.jpg",
"text": "aliquid luctus dolore",
"images": [],
"likes": 951,
"replyCount": 461,
"messages": 4,
"comments": [
{
"id": "73efb6fd-463c-4f9c-86ff-e4b588c6ad06",
"userNickname": "@region",
"userFullName": "Dennis Campbell",
"userAvatar": "file:///android_asset/avatars/186.jpg",
"text": "Solumlacinia scripta adhuc penatibus adversarium fuisset vix reque perpetua principes class. Menandridelenit reque verear tacimates novum hinc pericula animal dolore vis aliquam intellegebat idque cetero debet eirmod fusce. Scripseritcum lorem debet feugiat. Senseritarcu ea accusata oporteat expetendis leo morbi nulla pri hendrerit molestie vis leo cursus et movet pertinacia. Legimusassueverit malorum curabitur discere harum tale doctus mucius fuisset.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574147,
"reply": null,
"poll": null
},
{
"id": "a6d1ede1-e81f-4b0d-843b-8ed1844d7ba1",
"userNickname": "@penati",
"userFullName": "Antoinette Kent",
"userAvatar": "file:///android_asset/avatars/74.jpg",
"text": "Utinamsenectus risus. Metusligula indoctum postulant principes scripta luctus. Accusatalaudem scripserit pellentesque auctor offendit quas sodales intellegebat nunc.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774147,
"reply": null,
"poll": null
},
{
"id": "1d03eeaa-5c28-4b14-aee1-f3ce11317161",
"userNickname": "@iudica",
"userFullName": "Darwin Vasquez",
"userAvatar": "file:///android_asset/avatars/130.jpg",
"text": "debet wisi \n⏸👝 vix neque scelerisque \n👈♥️ 💽🚨 eloquentiam \n👤 🔭✏️ fermentum \n🍰 orci eum \n🍲🏪 salutatus eu qui \n👨❤️💋👨😺 🌬🥡 adolescens 📋 alia molestie verterem \n🚟📕 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774147,
"reply": null,
"poll": null
},
{
"id": "61a85f22-5be8-4654-bc79-33d54b3e3a14",
"userNickname": "@nisl",
"userFullName": "Lois Roberts",
"userAvatar": "file:///android_asset/avatars/185.jpg",
"text": "appareat feugait senectus \n⛵️ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574147,
"reply": null,
"poll": null
}
],
"createdAt": 1636968774147,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 902,
"positions": [
{
"text": "necessitatibus rutrum interpretaris quisque",
"voted": 373
},
{
"text": "harum qui",
"voted": 229
},
{
"text": "doming aliquet",
"voted": 300
}
]
}
},
"poll": null
},
{
"id": "b3144fba-c80c-4071-afa8-bcbc70e1c2d0",
"userNickname": "@quo",
"userFullName": "Merle Valencia",
"userAvatar": "file:///android_asset/avatars/33.jpg",
"text": "Molestiephasellus possim inimicus fuisset equidem similique fermentum sollicitudin auctor est adversarium. Sociisnulla eu.",
"images": [],
"likes": 530,
"replyCount": 132,
"messages": 2,
"comments": [
{
"id": "e7884073-64ac-4032-9b9f-15fac7b1512a",
"userNickname": "@socios",
"userFullName": "Julia Hogan",
"userAvatar": "file:///android_asset/avatars/0.jpg",
"text": "🔧⛓🌮 sociosqu🌷\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574147,
"reply": null,
"poll": null
},
{
"id": "d05711f0-55b5-4bd0-aba8-43cfd16ff52f",
"userNickname": "@ubique",
"userFullName": "Dale Bonner",
"userAvatar": "file:///android_asset/avatars/190.jpg",
"text": "commodo docendi",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174147,
"reply": null,
"poll": null
}
],
"createdAt": 1636997574147,
"reply": null,
"poll": null
},
{
"id": "397911dd-26cc-415b-b291-67216fda3be6",
"userNickname": "@possim",
"userFullName": "Aubrey Marsh",
"userAvatar": "file:///android_asset/avatars/157.jpg",
"text": "Posteasonet sumo tincidunt dolorem prompta propriae viderer periculis qualisque conclusionemque. Nostraquas persius congue dicant postea laudem partiendo magna ultricies noluisse at quidam volumus sadipscing donec omnesque urna. Reprimiquebibendum voluptatum harum verear sonet potenti dictumst pro alienum tibique electram volutpat. Elitrnatoque sea ea docendi urbanitas epicurei gloriatur mnesarchum mediocritatem pri lacinia interdum habitasse pellentesque tincidunt populo eros. Risusproin ferri posse.",
"images": [
"file:///android_asset/post_images/75.jpg",
"file:///android_asset/post_images/91.jpg"
],
"likes": 318,
"replyCount": 742,
"messages": 3,
"comments": [
{
"id": "7302cc5b-3949-40e4-9bfa-557267bc0f5b",
"userNickname": "@consti",
"userFullName": "Nicholas Berry",
"userAvatar": "file:///android_asset/avatars/78.jpg",
"text": "🦉😧 omittam \n👐 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637008374147,
"reply": null,
"poll": null
},
{
"id": "b6afb19a-ab4f-491b-a9f5-b51dcbc9c8c4",
"userNickname": "@omitta",
"userFullName": "Francesca Rosales",
"userAvatar": "file:///android_asset/avatars/122.jpg",
"text": "ubique eleifend dolor",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374147,
"reply": null,
"poll": null
},
{
"id": "8015b89e-198b-46a5-9a95-0020e34f09d1",
"userNickname": "@antiop",
"userFullName": "Kendra Sargent",
"userAvatar": "file:///android_asset/avatars/160.jpg",
"text": "💩🚉📞\nLaudemperpetua malesuada errem sed vehicula neque nominavi adhuc amet invidunt sit petentium agam auctor. Vericonsectetuer netus inani animal suavitate viderer ut facilisi commune populo expetendis non mi iisque te ad dicant. Optionmalesuada suas orci animal detraxit justo mus vocibus fabellas convenire duis ignota. Ridiculustellus efficitur inceptos eu faucibus periculis voluptaria erat consectetuer facilisis nulla platonem himenaeos dicit laudem dicta vis pulvinar ligula. Inveniresolet percipit graeci convenire gloriatur senectus interesset splendide consul recteque. Blanditintellegat enim nibh habeo esse omittam graece expetenda sociis nascetur platonem discere quo duis option dis nihil sem.\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774148,
"reply": null,
"poll": null
}
],
"createdAt": 1636957974147,
"reply": null,
"poll": null
},
{
"id": "bee5f509-c67e-4c35-ac86-67b53597ab05",
"userNickname": "@vero",
"userFullName": "Clayton Clark",
"userAvatar": "file:///android_asset/avatars/17.jpg",
"text": "👨👧👧🍣 tantas ante omnesque in\n",
"images": [],
"likes": 39,
"replyCount": 128,
"messages": 0,
"comments": [],
"createdAt": 1636975974148,
"reply": {
"id": "565c0d69-eb6e-4da1-891b-aeef16e85b2a",
"userNickname": "@detrax",
"userFullName": "Harriett Cherry",
"userAvatar": "file:///android_asset/avatars/131.jpg",
"text": "🏃🐲\nEgetancillae pertinax scripserit commodo postea neglegentur neque atomorum detraxit potenti aliquid. Meaocurreret mentitum has alterum posse rutrum mus vituperatoribus porttitor adversarium. Iriuresigniferumque delicata lacinia maiorum eleifend vel vix auctor dolores odio dicit persius finibus per.🚬🛑 ",
"images": [],
"likes": 548,
"replyCount": 27,
"messages": 7,
"comments": [
{
"id": "c805dd88-ce6e-4db9-8c0f-100c3a61574f",
"userNickname": "@adoles",
"userFullName": "Jonathon Ortiz",
"userAvatar": "file:///android_asset/avatars/22.jpg",
"text": "👩❤️💋👩🍐🐑\nappareat aliquam vidisse quod🥒🗄🚃 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774148,
"reply": null,
"poll": null
},
{
"id": "d1469d07-498f-4ad0-80d3-30ed243a743b",
"userNickname": "@auctor",
"userFullName": "Nelson Rojas",
"userAvatar": "file:///android_asset/avatars/92.jpg",
"text": "sit deterruisset veri 🐑🔖 maiestatis efficiantur no \n💨 mucius congue \n⁉️ 🎚↘️ nonumes \n🍳👨🌾 error conclusionemque 🐟 sociosqu semper sem \n🌮 👩👧👧🐉 pulvinar \n🙅🚤 🌇🏙 quidam 🤣 🚀👨👨👦👦 principes \n*️⃣🔣 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974148,
"reply": null,
"poll": null
},
{
"id": "268687b0-d1be-47cb-a1c4-90c68a61d8a3",
"userNickname": "@inani",
"userFullName": "Margarito McClain",
"userAvatar": "file:///android_asset/avatars/141.jpg",
"text": "suavitate theophrastus rutrum \n🐟🐸 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174148,
"reply": null,
"poll": null
},
{
"id": "a81fd42b-94fd-4268-9b0b-7626d593e252",
"userNickname": "@impetu",
"userFullName": "Denny Blackwell",
"userAvatar": "file:///android_asset/avatars/74.jpg",
"text": "Ridiculusamet maecenas epicurei viderer delicata class disputationi solet suscipit nonumy accusata. Dicitputent eius veritus epicuri eripuit repudiare postulant rutrum alterum repudiare eloquentiam prodesset adipisci consectetur.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374148,
"reply": null,
"poll": null
},
{
"id": "806aae03-0cc8-4642-9a1f-8388326626a9",
"userNickname": "@advers",
"userFullName": "Amy Richmond",
"userAvatar": "file:///android_asset/avatars/175.jpg",
"text": "💗\ndicam luctus regione mediocrem🚈🗜\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574148,
"reply": null,
"poll": null
},
{
"id": "a14ea282-a976-4ae4-b9ca-b8f1859b96d1",
"userNickname": "@necess",
"userFullName": "Earlene Blanchard",
"userAvatar": "file:///android_asset/avatars/113.jpg",
"text": "enim reprimique 🍔 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174148,
"reply": null,
"poll": null
},
{
"id": "ac6ace92-0e11-4b38-8be8-7beb56a5c47f",
"userNickname": "@reque",
"userFullName": "Cathy Reyes",
"userAvatar": "file:///android_asset/avatars/13.jpg",
"text": "🛸 ubique pri placerat persequeris quaerendum🍯🐍 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774148,
"reply": null,
"poll": null
}
],
"createdAt": 1636968774148,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "f5300f4a-935c-45fa-a169-a68fe801e3ac",
"userNickname": "@intere",
"userFullName": "Dwight Patterson",
"userAvatar": "file:///android_asset/avatars/72.jpg",
"text": "Consectetuertristique accumsan. Visinterdum mazim nihil facilisis delectus. Pertinaciafinibus noster dictumst neglegentur omittantur mediocrem faucibus nulla.",
"images": [
"file:///android_asset/post_images/49.jpg"
],
"likes": 620,
"replyCount": 213,
"messages": 9,
"comments": [
{
"id": "e578ce44-c5e3-460b-a9b3-7e92f5a95732",
"userNickname": "@purus",
"userFullName": "Kathy Walker",
"userAvatar": "file:///android_asset/avatars/92.jpg",
"text": "Tritanietiam te omnesque mentitum. Alienummorbi adolescens cum posidonium instructior. Noluissecivibus vim definitionem. Singulisunum civibus cubilia feugiat magna evertitur mediocrem dolore mollis regione. Imperdietvehicula partiendo errem reformidans error libero dictas harum aliquam. Dicatconstituto ante atqui affert tacimates suscipiantur sadipscing mattis ubique graeci donec assueverit posidonium morbi.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774148,
"reply": null,
"poll": null
},
{
"id": "739b6f77-0d12-4c88-8c7b-6289685f2c06",
"userNickname": "@cum",
"userFullName": "Robert Christian",
"userAvatar": "file:///android_asset/avatars/142.jpg",
"text": "ocurreret pertinacia in \n📓🦕 🌽⭐️ deserunt \n⛱ deserunt similique qui 🎄🏩 dui nisl 👘 equidem lorem 🌨🚁 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374148,
"reply": null,
"poll": null
},
{
"id": "9786b6bd-225f-490c-9262-0ff293c71bf7",
"userNickname": "@pellen",
"userFullName": "Christa Cole",
"userAvatar": "file:///android_asset/avatars/163.jpg",
"text": "Oporteathabitasse parturient vocibus has rhoncus interdum periculis sapientem adipiscing adhuc. Petentiumiuvaret facilisis legere duo interdum sententiae verear altera.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974148,
"reply": null,
"poll": null
},
{
"id": "e9788cc8-d2e0-4cab-a5a9-5aa278ea4240",
"userNickname": "@porta",
"userFullName": "Winifred Whitfield",
"userAvatar": "file:///android_asset/avatars/88.jpg",
"text": "expetenda purus volumus 🚑🤣 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774148,
"reply": null,
"poll": null
},
{
"id": "ab9e1884-1ebf-4df7-bdff-a22f986efaf0",
"userNickname": "@rhoncu",
"userFullName": "Ezra Berg",
"userAvatar": "file:///android_asset/avatars/46.jpg",
"text": "🐔\ninterdum bibendum interdum sollicitudin tale♑️🍤🍥\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774148,
"reply": null,
"poll": null
},
{
"id": "033e6fd2-1696-4cb5-a69a-246494c7fca9",
"userNickname": "@fastid",
"userFullName": "Lakeisha Puckett",
"userAvatar": "file:///android_asset/avatars/120.jpg",
"text": "🛴🦒\ninstructior🛵\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774148,
"reply": null,
"poll": null
},
{
"id": "77771810-85c8-4f98-b9c4-b915e11f3fb4",
"userNickname": "@senect",
"userFullName": "Cole Neal",
"userAvatar": "file:///android_asset/avatars/161.jpg",
"text": "🚝🚗 deterruisset \n✉️🥨 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374149,
"reply": null,
"poll": null
},
{
"id": "ebb40812-319a-4464-a3ab-6e15555b5d8e",
"userNickname": "@civibu",
"userFullName": "Claude Sweet",
"userAvatar": "file:///android_asset/avatars/4.jpg",
"text": "📀🐾🕙\nte facilisis🍟🏧\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574149,
"reply": null,
"poll": null
},
{
"id": "bff77c6b-b42f-442b-85b7-e23c5bbc8a4d",
"userNickname": "@justo",
"userFullName": "Mildred Blankenship",
"userAvatar": "file:///android_asset/avatars/130.jpg",
"text": "etiam maiestatis altera \n🚍🥣 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574149,
"reply": null,
"poll": null
}
],
"createdAt": 1636943574148,
"reply": null,
"poll": null
},
{
"id": "002d22e1-574b-4524-b5ee-8f312bfb8fc4",
"userNickname": "@errem",
"userFullName": "Dewayne Cook",
"userAvatar": "file:///android_asset/avatars/139.jpg",
"text": "ferri explicari nihil viris ea",
"images": [],
"likes": 902,
"replyCount": 453,
"messages": 2,
"comments": [
{
"id": "6e14d0c9-43c0-416a-9f41-c278033ce5a8",
"userNickname": "@te",
"userFullName": "Harold Cabrera",
"userAvatar": "file:///android_asset/avatars/118.jpg",
"text": "🍃🧀 Comprehensammovet viris graecis numquam dolor. Feugiatmaecenas dignissim putent vocibus volumus eum pulvinar appetere percipit disputationi dictum dictumst cetero referrentur adhuc. Ignotaelit imperdiet dolore sanctus wisi dissentiunt tellus dolorum antiopam vim dapibus detraxit sollicitudin id laoreet dico parturient maluisset tellus. ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974149,
"reply": null,
"poll": null
},
{
"id": "9b975255-3fe3-45c9-98f7-be3d5400ab14",
"userNickname": "@dicit",
"userFullName": "Salvatore Ayala",
"userAvatar": "file:///android_asset/avatars/131.jpg",
"text": "🥔🔠\ndetracto🌖🏰💔 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574149,
"reply": null,
"poll": null
}
],
"createdAt": 1637004774149,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 605,
"positions": [
{
"text": "voluptaria eu noster habeo",
"voted": 67
},
{
"text": "repudiare urbanitas",
"voted": 433
},
{
"text": "finibus libero dicunt",
"voted": 105
}
]
}
},
{
"id": "ba7dbba2-a411-4c83-9ebf-d577de836037",
"userNickname": "@option",
"userFullName": "Denver Crane",
"userAvatar": "file:///android_asset/avatars/155.jpg",
"text": "Diamdicant quaeque augue. Quisquem tantas augue ut. Vulputatevoluptaria altera ornare mutat nonumes mattis metus dictas vix tibique metus. Eirmoderos periculis contentiones debet eam fabellas neglegentur utroque faucibus vero reque neglegentur tota finibus ei definiebas finibus appareat. Proinsaepe risus.",
"images": [
"file:///android_asset/post_images/88.jpg"
],
"likes": 957,
"replyCount": 570,
"messages": 0,
"comments": [],
"createdAt": 1636939974149,
"reply": null,
"poll": null
},
{
"id": "4626e506-f7b9-4dcd-b572-8eb934ef6868",
"userNickname": "@idque",
"userFullName": "Angel Hamilton",
"userAvatar": "file:///android_asset/avatars/114.jpg",
"text": "est turpis",
"images": [],
"likes": 1,
"replyCount": 9,
"messages": 0,
"comments": [],
"createdAt": 1637011974149,
"reply": {
"id": "66915982-17b6-477a-8d4d-0486eab904b7",
"userNickname": "@compre",
"userFullName": "Adeline Simon",
"userAvatar": "file:///android_asset/avatars/29.jpg",
"text": "quo quem dicunt",
"images": [],
"likes": 86,
"replyCount": 651,
"messages": 3,
"comments": [
{
"id": "5d39d485-2070-4257-8324-b259c5be8a52",
"userNickname": "@commun",
"userFullName": "Lilia Barber",
"userAvatar": "file:///android_asset/avatars/174.jpg",
"text": "Deseruntvelit praesent mel maecenas aliquip urna pulvinar molestie option pri ludus duis sagittis. Aptentomittantur simul sociosqu repudiare interdum ornatus volumus veniam invidunt. Equidemtellus mus consul similique amet pellentesque integer quidam delicata vis sem. Dolorconstituam expetendis feugait explicari tibique platonem tamquam dignissim libris adversarium atomorum finibus ocurreret penatibus saperet splendide ius et condimentum.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174149,
"reply": null,
"poll": null
},
{
"id": "d392d041-16a6-4121-a8fa-a117f247d610",
"userNickname": "@singul",
"userFullName": "Hollie Collins",
"userAvatar": "file:///android_asset/avatars/30.jpg",
"text": "Leofabulas sagittis ponderum persius. Montesomnesque errem habitant litora atomorum quaeque convenire vulputate dicta patrioque discere consul oporteat aptent regione habeo. Offenditut maluisset instructior detraxit interpretaris vituperatoribus efficiantur corrumpit impetus eleifend intellegat. Usuinvidunt eruditi class vitae mucius fringilla fringilla tantas erroribus honestatis dicat. Veniammandamus ex molestiae nullam nihil neglegentur unum rhoncus ex ius euripidis eruditi tale tincidunt porro. Tractatoseget quaeque eius nihil justo appetere simul senserit conubia instructior mei ullamcorper periculis fermentum pretium diam prompta.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174149,
"reply": null,
"poll": null
},
{
"id": "c8b32f15-34c9-4e13-a9f3-b291ced68733",
"userNickname": "@sociis",
"userFullName": "Fannie Gill",
"userAvatar": "file:///android_asset/avatars/8.jpg",
"text": "wisi mei quem aeque",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774149,
"reply": null,
"poll": null
}
],
"createdAt": 1637004774149,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 937,
"positions": [
{
"text": "sapientem fastidii eos",
"voted": 334
},
{
"text": "appetere ludus invidunt",
"voted": 67
},
{
"text": "necessitatibus vituperata te sem sonet",
"voted": 107
},
{
"text": "conceptam",
"voted": 429
}
]
}
},
"poll": null
},
{
"id": "3fa3ad42-0d3d-4297-96ad-cb9fe50b1c8b",
"userNickname": "@conval",
"userFullName": "Rafael Sanford",
"userAvatar": "file:///android_asset/avatars/56.jpg",
"text": "epicurei lorem referrentur \n🍖 ",
"images": [],
"likes": 24,
"replyCount": 2,
"messages": 0,
"comments": [],
"createdAt": 1637001174149,
"reply": {
"id": "0ce8dde5-daf4-4a48-aa45-d29c20eb3af6",
"userNickname": "@venena",
"userFullName": "Valentin Crawford",
"userAvatar": "file:///android_asset/avatars/64.jpg",
"text": "💷🤷♂ vestibulum 🐔🎶 ",
"images": [],
"likes": 757,
"replyCount": 595,
"messages": 1,
"comments": [
{
"id": "d5831221-b46b-468b-9b9f-ec61dc2c2618",
"userNickname": "@perpet",
"userFullName": "Francesca Keller",
"userAvatar": "file:///android_asset/avatars/169.jpg",
"text": "Netusepicurei explicari an conceptam vidisse moderatius habitant eget fuisset ponderum interdum tritani theophrastus alia. Dicatrhoncus vituperata salutatus tractatos metus eros possit saepe homero putent no mazim quaerendum.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374149,
"reply": null,
"poll": null
}
],
"createdAt": 1637001174149,
"reply": null,
"poll": {
"isCompleted": false,
"totalVotes": 1047,
"positions": [
{
"text": "eloquentiam affert sapien",
"voted": 86
},
{
"text": "reprimique elit ridiculus",
"voted": 470
},
{
"text": "sea mea habemus bibendum consetetur",
"voted": 491
}
]
}
},
"poll": null
},
{
"id": "ec886536-2887-4e63-961f-0d8282559ba5",
"userNickname": "@et",
"userFullName": "Eli Robbins",
"userAvatar": "file:///android_asset/avatars/4.jpg",
"text": "🏝💡\nmaiorum delicata adversarium\n",
"images": [],
"likes": 40,
"replyCount": 96,
"messages": 0,
"comments": [],
"createdAt": 1637011974149,
"reply": {
"id": "8d4a370f-df9f-44db-b838-4139f6c635fb",
"userNickname": "@etiam",
"userFullName": "Chris Alvarez",
"userAvatar": "file:///android_asset/avatars/126.jpg",
"text": "🕜🍡👖 Augueatomorum consectetuer altera laudem delectus mus nonumy sollicitudin eum dapibus viverra volutpat viris feugiat. Quascubilia autem recteque elementum urna per deterruisset utamur adhuc nobis sapientem persequeris cubilia. Pertinaciadelicata lacus. Autemfastidii suavitate pulvinar parturient adipiscing deseruisse taciti simul brute decore eleifend invidunt aliquam dolore.🍪🚖 ",
"images": [],
"likes": 485,
"replyCount": 621,
"messages": 0,
"comments": [],
"createdAt": 1637008374149,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "6c3fef87-b01b-447e-9628-148a7791e7d9",
"userNickname": "@nihil",
"userFullName": "Kristi Walter",
"userAvatar": "file:///android_asset/avatars/98.jpg",
"text": "♨️📈 class \n🚓💳 ",
"images": [],
"likes": 10,
"replyCount": 56,
"messages": 0,
"comments": [],
"createdAt": 1636997574149,
"reply": {
"id": "4c665a2f-280e-457b-b78b-ae56f508665f",
"userNickname": "@cum",
"userFullName": "Delbert Harrell",
"userAvatar": "file:///android_asset/avatars/135.jpg",
"text": "😣\nSuscipianturauctor magna efficitur ridens cu erroribus volutpat erroribus utamur sadipscing constituto donec brute adolescens. Reprimiquequod praesent porro blandit varius periculis senectus aliquip vix alia numquam. Sedpersecuti pretium varius iusto nunc malesuada scelerisque iusto nibh. Graeceomnesque tacimates iudicabit consul libero discere eius eam voluptatum dolores ac sumo lobortis alia nostra interdum graeco.🎁🌿🚈\n",
"images": [
"file:///android_asset/post_images/22.jpg"
],
"likes": 460,
"replyCount": 163,
"messages": 2,
"comments": [
{
"id": "62333c1b-a067-4ebe-a221-523b9a74300c",
"userNickname": "@necess",
"userFullName": "Stanley Kennedy",
"userAvatar": "file:///android_asset/avatars/101.jpg",
"text": "Turpisjusto vituperata netus vis latine wisi esse quis. Maluissetvidisse perpetua quis luptatum recteque potenti ei sodales saepe quisque eu electram mediocrem ignota tritani. Cuorci urna solum mandamus. Vocibusinceptos sit parturient repudiandae posuere natum lectus vel fames libris tellus qualisque. Graecemandamus adversarium mea referrentur indoctum nostrum. Fugitlaoreet voluptatibus dolores ut condimentum wisi sale expetenda voluptaria posse aptent eos ponderum.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174149,
"reply": null,
"poll": null
},
{
"id": "6e3216b1-0204-40fc-972b-c5ed19503caa",
"userNickname": "@vero",
"userFullName": "Alexis Morton",
"userAvatar": "file:///android_asset/avatars/64.jpg",
"text": "mauris aliquid reprimique eleifend",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374149,
"reply": null,
"poll": null
}
],
"createdAt": 1636990374149,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "5952c49a-1af6-4ffa-b365-ffe1971b2fd6",
"userNickname": "@quisqu",
"userFullName": "Dominick Nielsen",
"userAvatar": "file:///android_asset/avatars/54.jpg",
"text": "🏨 ferri scelerisque ligula🦈🐸 ",
"images": [],
"likes": 46,
"replyCount": 117,
"messages": 0,
"comments": [],
"createdAt": 1636961574150,
"reply": {
"id": "fb5f346f-ffc8-48f7-ab1a-fab07b706d9c",
"userNickname": "@alienu",
"userFullName": "Nannie Simmons",
"userAvatar": "file:///android_asset/avatars/136.jpg",
"text": "Legeremenandri numquam detracto postulant eu fugit novum feugiat scripta solet utamur. Maiestatisassueverit impetus urbanitas. Fuissetoratio molestie sed reque consul mucius noluisse mauris harum natoque mediocrem doming ridiculus invenire. Mediocritatemquis possit accusata integer ceteros donec consequat contentiones efficiantur alienum dicunt phasellus taciti efficitur animal vituperata. Suspendisseenim habitant falli expetenda consequat velit utinam saperet efficitur ultrices oratio esse noster scripserit mollis dolorem parturient vix.",
"images": [],
"likes": 995,
"replyCount": 238,
"messages": 7,
"comments": [
{
"id": "e698d6be-9846-4f7a-87f4-d64a3ebabf6e",
"userNickname": "@ius",
"userFullName": "Laverne Glass",
"userAvatar": "file:///android_asset/avatars/4.jpg",
"text": "constituam pertinax 😤🏷 maiestatis intellegat \n🚥 ridiculus maluisset \n🆕😲 splendide constituto 🚅 turpis audire tristique \n🎑⛓ 🛫🍕 mutat 🌸 🍈👱 sententiae ⭐️ curabitur sociosqu salutatus \n📷🤳 deseruisse sit 💛 suspendisse id ❗️🍓 vidisse tritani 🌅 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174150,
"reply": null,
"poll": null
},
{
"id": "9baf5408-cac1-4ea5-bc70-0c74d61dc090",
"userNickname": "@antiop",
"userFullName": "Jenny Becker",
"userAvatar": "file:///android_asset/avatars/135.jpg",
"text": "Mutatelit senserit adversarium platonem minim principes sumo labores dicat noster magnis. Ponderumplatonem pharetra mazim mucius risus. Etiamtorquent fuisset graeco morbi delectus patrioque dictas pharetra saperet sententiae quem pri. Dignissimpossim civibus quo luctus detracto scripserit dolores convallis cubilia senserit tota eos elit elementum postulant numquam. Meliorealtera qui euripidis class ultricies lorem adipisci pharetra epicurei atomorum iisque quo discere condimentum sapientem wisi odio. Quaestiointeger detraxit himenaeos inimicus splendide perpetua inani velit maiestatis sapien quo lorem commune elaboraret nobis finibus tantas.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174150,
"reply": null,
"poll": null
},
{
"id": "c14ae551-679d-42a4-867a-566e34509ddd",
"userNickname": "@inimic",
"userFullName": "Candy Levine",
"userAvatar": "file:///android_asset/avatars/147.jpg",
"text": "moderatius ipsum",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374150,
"reply": null,
"poll": null
},
{
"id": "8bdd7a20-f256-49c7-9ecb-bd0d65458882",
"userNickname": "@cu",
"userFullName": "Emmanuel Ware",
"userAvatar": "file:///android_asset/avatars/149.jpg",
"text": "▫️📝🗓 minim🏣\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374150,
"reply": null,
"poll": null
},
{
"id": "d2ce4590-8771-4b66-a659-5be7a22ac528",
"userNickname": "@urbani",
"userFullName": "Rickie Beach",
"userAvatar": "file:///android_asset/avatars/38.jpg",
"text": "aliquam bibendum euripidis",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974150,
"reply": null,
"poll": null
},
{
"id": "aa1c85f7-3915-4811-b417-5678ee15d3a8",
"userNickname": "@splend",
"userFullName": "Arthur Payne",
"userAvatar": "file:///android_asset/avatars/29.jpg",
"text": "Dapibuspertinacia curae ius intellegat scelerisque ubique gravida facilis pro inciderint maecenas tantas luptatum viris facilisi id sapien augue. Sagittisexpetendis fabellas mandamus lobortis definitionem indoctum persecuti est epicurei eirmod litora voluptaria phasellus feugait cu partiendo donec agam.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974150,
"reply": null,
"poll": null
},
{
"id": "d81ae8e4-c8e8-4c07-ae51-17b2439cbeb9",
"userNickname": "@luctus",
"userFullName": "Elma Harding",
"userAvatar": "file:///android_asset/avatars/12.jpg",
"text": "Sollicitudindeseruisse veritus dolore ridiculus interdum comprehensam agam dicat ex auctor similique lacinia et wisi. Eiusverear eos hinc mus perpetua necessitatibus meliore porta iuvaret rutrum sodales ei tempus ornare. Dolordolore fabulas nostra fugit doctus ignota pertinax constituto habitant ridens deterruisset sale pri. Maluissetduis curae assueverit consetetur iisque cetero eget option finibus. Facilisilaudem libris disputationi vitae ancillae idque turpis risus nullam utinam utamur nibh inimicus quisque gravida eruditi. Assueverittempor discere falli accumsan propriae persequeris vivamus eu hac sapientem mentitum scripta aliquip interesset scripta latine hac augue tale.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174150,
"reply": null,
"poll": null
}
],
"createdAt": 1636947174150,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "3426ed85-bf64-45aa-97e4-a365c09ef474",
"userNickname": "@nisl",
"userFullName": "Jeannette Benton",
"userAvatar": "file:///android_asset/avatars/112.jpg",
"text": "ridiculus nihil \n🍂🐕 🌒🍜 mollis \n🍞 🈁♒️ conubia \n📙🚈 🗂🐋 inciderint ⌚📯 👧➕ fermentum \n🍧⛏ ⛎🔕 constituam ⛓🔚 ",
"images": [],
"likes": 9,
"replyCount": 651,
"messages": 4,
"comments": [
{
"id": "003ff973-370a-413b-b14c-25cc71f8503b",
"userNickname": "@unum",
"userFullName": "Pete Ruiz",
"userAvatar": "file:///android_asset/avatars/75.jpg",
"text": "🥡⚛️🍙 Duispulvinar constituam regione platonem delectus persius mutat primis dicam. Eatamquam vivamus in noluisse omnesque in harum gloriatur novum vehicula aeque has iudicabit wisi labores ornare platea reprehendunt. Auguequem molestie cubilia fames dapibus inimicus.👩👧👧🎛 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974150,
"reply": null,
"poll": null
},
{
"id": "4368a1ae-33ab-4cd2-8f1b-fede45544e91",
"userNickname": "@euripi",
"userFullName": "Hilda Woods",
"userAvatar": "file:///android_asset/avatars/108.jpg",
"text": "🍭‼️ Malorummoderatius postea lacinia eloquentiam petentium ne facilis imperdiet iusto ceteros quis risus sit novum malorum petentium sagittis. Idqueunum vituperata eam eget vidisse posidonium tortor egestas nonumes. Habeodicunt regione moderatius graecis impetus pro facilis eos ponderum prompta pertinax blandit a tale dignissim. Auguequas evertitur duo eros erat. Ansenectus mediocritatem libris laudem cum nostrum ante ornatus audire urbanitas at civibus volutpat tempor felis.\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374150,
"reply": null,
"poll": null
},
{
"id": "24599e9c-cdf3-49e8-99ab-242c4d45bab9",
"userNickname": "@autem",
"userFullName": "Christie Bennett",
"userAvatar": "file:///android_asset/avatars/28.jpg",
"text": "senserit an 🚒🎆 🥠🚞 vis 🍜 🥒🗼 harum 🖥 dolorum vituperata nascetur \n🦗 quot sodales \n👨😍 civibus nobis \n🌷 🚅🥡 delenit \n🗑🎩 🦃📊 nam \n🈹 🥠🚢 venenatis 🦅🗼 veri liber tempus 🔎⛄️ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574150,
"reply": null,
"poll": null
},
{
"id": "2e229a6d-ce36-49fc-97e7-c17c9ed8a72c",
"userNickname": "@vocibu",
"userFullName": "Corey Sykes",
"userAvatar": "file:///android_asset/avatars/95.jpg",
"text": "Arcubibendum vulputate risus pretium mel imperdiet vitae taciti quaeque ne consul autem nibh pertinax. Electramsententiae definitiones mei interpretaris. Hachomero civibus inani discere decore dissentiunt idque. Quodmeliore ante inceptos vulputate aperiri platea aperiri blandit mus patrioque. Deterruissetdetraxit perpetua expetenda inceptos aliquid euismod persecuti netus scripserit convallis nonumy potenti curabitur mnesarchum. Turpisvituperata cubilia dicam praesent fabellas egestas eirmod.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774150,
"reply": null,
"poll": null
}
],
"createdAt": 1636983174150,
"reply": null,
"poll": null
},
{
"id": "cca6ff89-1cde-4a04-808b-229af0be1157",
"userNickname": "@usu",
"userFullName": "Anastasia Lang",
"userAvatar": "file:///android_asset/avatars/135.jpg",
"text": "📛🦋🛋 Sollicitudinerat quisque turpis a legimus equidem. Nibhpri nihil altera populo porro docendi decore maecenas assueverit postulant maiestatis nec vestibulum delenit. Dicuntnulla nominavi graeci graeco usu voluptaria enim condimentum possit detraxit turpis habitasse unum scripta tempor lobortis taciti pellentesque suspendisse. Antecurae dui conubia senserit urbanitas vocent interdum solet pharetra. Sapienmnesarchum quot. Tamquamlegimus eius error scripta dicit eleifend hinc autem laudem maluisset vim aliquet dicta inani legere sea velit ei.👬💠🏨\n",
"images": [
"file:///android_asset/post_images/77.jpg"
],
"likes": 667,
"replyCount": 103,
"messages": 2,
"comments": [
{
"id": "6b349d2a-0520-4a05-b913-463102042b43",
"userNickname": "@vocent",
"userFullName": "Dollie Ashley",
"userAvatar": "file:///android_asset/avatars/178.jpg",
"text": "🔐🈂️🗽\niusto dicant🏰🐫 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774151,
"reply": null,
"poll": null
},
{
"id": "262814bf-4007-49c5-a6b5-6a8d1022e060",
"userNickname": "@honest",
"userFullName": "Brandie Salinas",
"userAvatar": "file:///android_asset/avatars/60.jpg",
"text": "proin tellus pretium euismod elitr",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174151,
"reply": null,
"poll": null
}
],
"createdAt": 1636939974150,
"reply": null,
"poll": null
},
{
"id": "750d51cf-ff30-408b-8630-bf8d404e159d",
"userNickname": "@vocent",
"userFullName": "Bernadine Everett",
"userAvatar": "file:///android_asset/avatars/152.jpg",
"text": "nonumy nonumy 😎 ",
"images": [],
"likes": 7,
"replyCount": 17,
"messages": 0,
"comments": [],
"createdAt": 1636947174151,
"reply": {
"id": "c2fe178e-cb80-4c87-88d9-c8d912d67e27",
"userNickname": "@possit",
"userFullName": "Ivy Olson",
"userAvatar": "file:///android_asset/avatars/113.jpg",
"text": "adversarium dolorem porttitor 🌌🏦 🔗🕯 atqui \n👣🕒 ",
"images": [
"file:///android_asset/post_images/8.jpg",
"file:///android_asset/post_images/0.jpg"
],
"likes": 873,
"replyCount": 309,
"messages": 3,
"comments": [
{
"id": "746c4ecd-4c17-44b8-af26-564bd09e0ed6",
"userNickname": "@electr",
"userFullName": "Morgan Riggs",
"userAvatar": "file:///android_asset/avatars/74.jpg",
"text": "corrumpit primis indoctum \n🔴 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974151,
"reply": null,
"poll": null
},
{
"id": "5594c4d3-cd3c-44d5-8e03-4be16019124e",
"userNickname": "@dicam",
"userFullName": "Ola Griffin",
"userAvatar": "file:///android_asset/avatars/80.jpg",
"text": "🥣📱 phasellus 🐥🐺 fabellas saperet sententiae \n⏰🌻 🍖🚚 massa 🥤 gubergren cetero maluisset \n🚱🍋 🍡👎 dapibus \n🖲👁🗨 💃🌠 contentiones \n🍢🍽 🧦🎙 omittantur 🐡🥗 molestie alterum ligula 🍞 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174151,
"reply": null,
"poll": null
},
{
"id": "d1977208-08aa-44c7-aa9d-9509c54dd6c3",
"userNickname": "@verter",
"userFullName": "Michel Lott",
"userAvatar": "file:///android_asset/avatars/150.jpg",
"text": "🥘💽\nsaepe deserunt sadipscing🖨\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174151,
"reply": null,
"poll": null
}
],
"createdAt": 1636943574151,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "f9ff15fc-c768-415f-b751-f64002c50e99",
"userNickname": "@at",
"userFullName": "Amy Noble",
"userAvatar": "file:///android_asset/avatars/7.jpg",
"text": "Persiusquot cu comprehensam. Liberpropriae contentiones mus eum platonem affert dolore velit aliquip dis nisl. Orcimauris dolorem movet augue aliquet facilisi. Vitaeelementum taciti antiopam splendide aliquip sem discere doming urbanitas usu signiferumque cursus nihil purus corrumpit. Suspendissewisi malorum iriure nominavi prodesset platonem expetenda idque netus convallis. Theophrastuslectus potenti placerat cetero harum labores nec agam nonumy sententiae quam necessitatibus hinc senserit eu viverra potenti.",
"images": [],
"likes": 583,
"replyCount": 401,
"messages": 2,
"comments": [
{
"id": "1f60e1ce-f1e6-467a-86cd-60c2ba7b5860",
"userNickname": "@ut",
"userFullName": "Meredith Arnold",
"userAvatar": "file:///android_asset/avatars/32.jpg",
"text": "Suscipianturfelis maecenas adversarium justo lacinia pellentesque scelerisque ridens phasellus aliquam. Comprehensamnetus aliquid omnesque dis urna possim fastidii usu felis faucibus luctus neque audire vidisse fusce option arcu. Possedocendi mea sem fusce constituto persecuti aperiri adversarium quisque elitr viderer taciti parturient. Blanditbibendum docendi lacus altera habemus dolor integer.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974151,
"reply": null,
"poll": null
},
{
"id": "bfdb474d-e472-480e-88cd-51f318ef727a",
"userNickname": "@conclu",
"userFullName": "Chadwick Bush",
"userAvatar": "file:///android_asset/avatars/61.jpg",
"text": "🚧🗼 leo 🕵 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174151,
"reply": null,
"poll": null
}
],
"createdAt": 1636939974151,
"reply": null,
"poll": null
},
{
"id": "6a808537-26b0-4f13-a70e-39b9edb0cfe2",
"userNickname": "@litora",
"userFullName": "Mary Arnold",
"userAvatar": "file:///android_asset/avatars/179.jpg",
"text": "nostrum proin 🈂️ dictum decore 🎋 inani ocurreret 🏫😁 👒💂 iaculis \n🎉⏱ sanctus phasellus doctus 📤 noluisse alterum ⏸🔼 delectus nascetur \n🍹 purus tractatos \n😡✊ pharetra laudem maluisset \n🚡🚫 ",
"images": [],
"likes": 625,
"replyCount": 365,
"messages": 7,
"comments": [
{
"id": "94363a81-4e98-4e32-be6e-e6c97b009a52",
"userNickname": "@nulla",
"userFullName": "Ignacio Kidd",
"userAvatar": "file:///android_asset/avatars/117.jpg",
"text": "movet",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574151,
"reply": null,
"poll": null
},
{
"id": "449c6a04-4403-46e9-9776-0a522f89ea2c",
"userNickname": "@cetero",
"userFullName": "Jasmine Perez",
"userAvatar": "file:///android_asset/avatars/60.jpg",
"text": "Atquicetero primis tortor qui sadipscing felis tristique ridiculus consectetur repudiandae salutatus regione tota detracto ea omittam velit. Meipostea graece discere suscipiantur labores inciderint eros. Ridiculusdelenit noster iuvaret graece platonem referrentur pellentesque habitant appareat appetere. Quinisi massa ornatus ex noster vim.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636972374151,
"reply": null,
"poll": null
},
{
"id": "b3e3ae8f-2229-4bbd-b42d-41f34a83ae0b",
"userNickname": "@repudi",
"userFullName": "Florine Finch",
"userAvatar": "file:///android_asset/avatars/127.jpg",
"text": "habitant latine convallis discere mauris",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974151,
"reply": null,
"poll": null
},
{
"id": "dfedcb88-d1a9-4d53-a29a-270559876779",
"userNickname": "@medioc",
"userFullName": "Delbert Forbes",
"userAvatar": "file:///android_asset/avatars/169.jpg",
"text": "placerat urbanitas metus",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574151,
"reply": null,
"poll": null
},
{
"id": "e81e481c-5e1e-436b-8daa-66803314cefb",
"userNickname": "@phasel",
"userFullName": "Lynette Grant",
"userAvatar": "file:///android_asset/avatars/25.jpg",
"text": "Adolescensveritus consetetur veri dapibus proin delenit theophrastus impetus theophrastus cursus deterruisset. Periculismus his dissentiunt conceptam urbanitas theophrastus dictas at metus consequat simul nullam. Salutatusphasellus luctus curae mentitum evertitur pro eum quis dictum dicunt possim pericula tacimates alia eruditi luctus. Dictassonet antiopam doming ludus esse vestibulum sagittis sadipscing falli audire habitant. Sagittisveritus brute iudicabit atqui vero curae. Utroquenoster omittam.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774151,
"reply": null,
"poll": null
},
{
"id": "901cef8b-423d-44e2-8d98-e3a01cc30eb8",
"userNickname": "@enim",
"userFullName": "Elinor Strong",
"userAvatar": "file:///android_asset/avatars/24.jpg",
"text": "Estrisus persequeris sit qualisque ad amet tristique tritani iriure commune senectus suscipiantur fabellas utamur cu liber aliquip viverra. Putentdicta movet reformidans scelerisque detracto utamur utroque dis laudem ut an maiestatis atomorum rutrum mel nam impetus. Fermentumerroribus legere lorem aenean delenit vix primis leo mollis aliquet dicat populo eos ante dolore prompta vix. Dictassanctus an neglegentur theophrastus quaestio deterruisset tristique epicuri facilisi mauris faucibus autem morbi patrioque. Reformidansutamur lobortis dico nam fugit ridens volumus potenti causae omittantur posidonium dicam docendi commodo habeo elitr.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374151,
"reply": null,
"poll": null
},
{
"id": "35411339-5abf-45fd-9a10-b585f0a306ee",
"userNickname": "@medioc",
"userFullName": "Sonny Walker",
"userAvatar": "file:///android_asset/avatars/152.jpg",
"text": "🍼 Dolorummaiestatis definitiones ante adversarium appareat ante vocent repudiandae ultricies dictas nonumes vituperata. Populoquem nam nostra mentitum interdum antiopam convenire ridens vitae epicurei metus nihil civibus. Hasius persecuti vulputate mollis volumus equidem prompta luptatum dolorum assueverit possit. Justomaluisset cu sociosqu agam elit tincidunt debet noluisse alienum facilis. Duifacilisis ridiculus antiopam rhoncus fuisset postea nec eruditi. ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174152,
"reply": null,
"poll": null
}
],
"createdAt": 1637001174151,
"reply": null,
"poll": null
},
{
"id": "2f393e6b-9a06-4c7a-8119-389c9d514aa7",
"userNickname": "@sea",
"userFullName": "Margo Carpenter",
"userAvatar": "file:///android_asset/avatars/81.jpg",
"text": "Posidoniumligula a. Sitludus maximus tale iisque ac.",
"images": [
"file:///android_asset/post_images/9.jpg",
"file:///android_asset/post_images/20.jpg"
],
"likes": 433,
"replyCount": 430,
"messages": 2,
"comments": [
{
"id": "fac5c998-719e-4273-9172-d71744f97c62",
"userNickname": "@verear",
"userFullName": "Ingrid Alvarez",
"userAvatar": "file:///android_asset/avatars/40.jpg",
"text": "🎩 scripta mucius pellentesque nam🕐🛶😎 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974152,
"reply": null,
"poll": null
},
{
"id": "e00f833f-a594-44d9-9ec2-6d1190cb259c",
"userNickname": "@movet",
"userFullName": "Vaughn Quinn",
"userAvatar": "file:///android_asset/avatars/119.jpg",
"text": "Nasceturfinibus ultricies laoreet deserunt iudicabit tristique labores convallis affert tristique dictum mauris dis sonet vulputate nobis senectus tristique egestas. Debetantiopam quis. Inanimollis dictumst menandri inani cu quod elitr lectus dolorum fugit ceteros audire magna imperdiet. Voluptatumcum ocurreret dolore ocurreret vidisse quaerendum explicari pri potenti nam periculis fabellas curabitur nulla voluptatum nullam nostra numquam dolorem. Nonporta quaestio utroque.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774152,
"reply": null,
"poll": null
}
],
"createdAt": 1636947174152,
"reply": null,
"poll": null
},
{
"id": "b48bb561-7b18-479e-86ef-e5a11b8ff288",
"userNickname": "@mutat",
"userFullName": "Staci Guerrero",
"userAvatar": "file:///android_asset/avatars/55.jpg",
"text": "veri est ☣️ ",
"images": [],
"likes": 43,
"replyCount": 198,
"messages": 0,
"comments": [],
"createdAt": 1636975974152,
"reply": {
"id": "b98be86c-60c3-4468-96d2-3ef2e8a2543b",
"userNickname": "@aeque",
"userFullName": "Dylan Moses",
"userAvatar": "file:///android_asset/avatars/76.jpg",
"text": "fusce melius habeo",
"images": [],
"likes": 786,
"replyCount": 684,
"messages": 0,
"comments": [],
"createdAt": 1636961574152,
"reply": null,
"poll": {
"isCompleted": false,
"totalVotes": 683,
"positions": [
{
"text": "a atqui ocurreret",
"voted": 179
},
{
"text": "cras tristique cu eam",
"voted": 152
},
{
"text": "explicari turpis suavitate",
"voted": 26
},
{
"text": "atqui mandamus ea volutpat epicurei",
"voted": 326
}
]
}
},
"poll": null
},
{
"id": "adda6e7c-997a-4a96-a7b2-f2bbeb995f55",
"userNickname": "@possit",
"userFullName": "Julius Dean",
"userAvatar": "file:///android_asset/avatars/125.jpg",
"text": "graeci",
"images": [],
"likes": 49,
"replyCount": 461,
"messages": 1,
"comments": [
{
"id": "dc6e89e2-a0b8-4bbb-9141-d2009afaa810",
"userNickname": "@tale",
"userFullName": "Sammie Wooten",
"userAvatar": "file:///android_asset/avatars/38.jpg",
"text": "pericula",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974152,
"reply": null,
"poll": null
}
],
"createdAt": 1636968774152,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 1316,
"positions": [
{
"text": "mi aptent voluptatibus nullam aliquip",
"voted": 207
},
{
"text": "tristique dapibus homero nunc tation",
"voted": 412
},
{
"text": "augue graece dignissim idque torquent",
"voted": 253
},
{
"text": "quod mei sociis intellegebat ornare",
"voted": 309
},
{
"text": "graeci eam curabitur",
"voted": 135
}
]
}
},
{
"id": "ceb37ce9-751c-4332-bf31-725cf41bbbc5",
"userNickname": "@senser",
"userFullName": "Gayle Francis",
"userAvatar": "file:///android_asset/avatars/76.jpg",
"text": "🛄 Postulantmontes iudicabit numquam verterem reprimique meliore offendit bibendum possit sociosqu. Senseritalia sumo necessitatibus moderatius melius sociis detracto iaculis maecenas nonumes luptatum nostra sale fugit maximus fastidii. Erosnostrum risus amet animal eripuit sanctus sea intellegat intellegebat ius latine platonem deserunt. ",
"images": [],
"likes": 122,
"replyCount": 662,
"messages": 7,
"comments": [
{
"id": "cd31c18b-28b5-4226-a72b-25991a22f613",
"userNickname": "@propri",
"userFullName": "Marlin Daniels",
"userAvatar": "file:///android_asset/avatars/172.jpg",
"text": "Magnamei efficitur fermentum ferri repudiandae lobortis sagittis mus quisque vituperatoribus. Suavitateusu persecuti ius adipiscing. Senseritsanctus legere efficitur eos consul nihil curabitur pretium propriae nominavi diam dicunt. Vituperatanetus melius contentiones singulis eum postulant graecis nibh voluptatum neque hac.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574152,
"reply": null,
"poll": null
},
{
"id": "ea6ef001-a154-4890-9ff4-4115853b459b",
"userNickname": "@quaest",
"userFullName": "Josef McMahon",
"userAvatar": "file:///android_asset/avatars/84.jpg",
"text": "repudiandae dolores \n👨✈ consectetur autem arcu \n🎠💒 similique quas \n🥙 eius suscipit eu 🥙 quem accusata \n🔹🕋 inimicus oporteat \n🔒 ac splendide nonumy \n🐕 🥙▫️ ancillae \n🕡 ⛈👅 nec 🍫 laoreet augue gravida \n🐳🌺 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174152,
"reply": null,
"poll": null
},
{
"id": "b142ef5e-9e60-4467-8039-da227f2b3a38",
"userNickname": "@princi",
"userFullName": "Aline Espinoza",
"userAvatar": "file:///android_asset/avatars/144.jpg",
"text": "legere fames",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374152,
"reply": null,
"poll": null
},
{
"id": "9867c9ff-f5ea-4952-bc50-4ddd83b029a5",
"userNickname": "@nunc",
"userFullName": "Deloris McMahon",
"userAvatar": "file:///android_asset/avatars/114.jpg",
"text": "🔦\nMelsemper meliore voluptatum numquam nulla sonet possit civibus eos. Egetaenean saperet suas felis ludus fusce conclusionemque taciti dicant diam urna maluisset utamur alia rutrum quo sadipscing. Elementumviverra nonumes solum nisl turpis his suas mandamus feugait. Conguemauris tation nisi dissentiunt dolorum populo menandri rutrum quo eget pulvinar. Massalibero consectetuer dolore magnis habemus natoque. ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774152,
"reply": null,
"poll": null
},
{
"id": "1f3be7fa-d4d6-4aae-89f3-8d0ba42f2ad8",
"userNickname": "@feugai",
"userFullName": "Mitchell Cantrell",
"userAvatar": "file:///android_asset/avatars/18.jpg",
"text": "🌩📧\nPostulantveri tempus. Appareatnibh dolore movet minim ceteros duo propriae augue aeque magna prompta tempor mediocrem potenti accusata gubergren idque. Donecdiam posuere rutrum cu pellentesque ad habitasse putent graeco blandit animal posuere. Nostraexplicari elitr populo dolore adipiscing vivamus feugiat. Pellentesquemalorum quaeque. Ornarelaudem malesuada libris dictum lectus dicit quaerendum electram curae ponderum efficitur eum numquam consul doctus delicata graecis convenire.🐵🥟\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974152,
"reply": null,
"poll": null
},
{
"id": "cac9454f-c0b6-448a-bd01-368c4c742b29",
"userNickname": "@sollic",
"userFullName": "Cornelius Cunningham",
"userAvatar": "file:///android_asset/avatars/139.jpg",
"text": "🦂🍀🌧 antiopam patrioque tellus fermentum🌬✋ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174152,
"reply": null,
"poll": null
},
{
"id": "e979e7d2-8846-40e7-ade3-bfef6835d9ad",
"userNickname": "@pericu",
"userFullName": "Cindy Christian",
"userAvatar": "file:///android_asset/avatars/68.jpg",
"text": "🏪\ndebet atqui autem🖨💠\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774152,
"reply": null,
"poll": null
}
],
"createdAt": 1636947174152,
"reply": null,
"poll": null
},
{
"id": "a715f45c-0ba5-42c8-9658-46623c4c7f64",
"userNickname": "@movet",
"userFullName": "Rachelle Bonner",
"userAvatar": "file:///android_asset/avatars/91.jpg",
"text": "lectus verterem libris",
"images": [],
"likes": 769,
"replyCount": 471,
"messages": 2,
"comments": [
{
"id": "3f8b96ec-0311-4ad7-87f7-90b1315662ef",
"userNickname": "@maiest",
"userFullName": "Felix Vaughn",
"userAvatar": "file:///android_asset/avatars/31.jpg",
"text": "🦏☂️ Voluptariaiuvaret homero adversarium persius simul lobortis sagittis varius omnesque simul senectus equidem tempus senserit. Inceptosmelius suavitate molestie. Magnisindoctum similique omittam latine.👍 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174152,
"reply": null,
"poll": null
},
{
"id": "666f0e59-8c47-4baa-9805-01931991401f",
"userNickname": "@utroqu",
"userFullName": "Yvette Hammond",
"userAvatar": "file:///android_asset/avatars/179.jpg",
"text": "Audirenisl brute metus convallis maiorum suas honestatis tation pri risus. Mediocremest convenire diam persecuti eam an verterem ferri.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174152,
"reply": null,
"poll": null
}
],
"createdAt": 1636986774152,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 974,
"positions": [
{
"text": "partiendo sapien",
"voted": 187
},
{
"text": "odio discere",
"voted": 152
},
{
"text": "placerat eos ignota adhuc aliquid",
"voted": 228
},
{
"text": "liber ancillae consetetur neque an",
"voted": 284
},
{
"text": "solum postea gloriatur",
"voted": 123
}
]
}
},
{
"id": "19f819fd-86df-4391-874e-d5d73f92622e",
"userNickname": "@inveni",
"userFullName": "Mona Cardenas",
"userAvatar": "file:///android_asset/avatars/95.jpg",
"text": "🍌🌊 deserunt \n🚚 ",
"images": [],
"likes": 45,
"replyCount": 87,
"messages": 0,
"comments": [],
"createdAt": 1636968774153,
"reply": {
"id": "3ea2bea3-bcfd-45df-96ba-4297f703bc99",
"userNickname": "@primis",
"userFullName": "Pauline Pruitt",
"userAvatar": "file:///android_asset/avatars/188.jpg",
"text": "🗻🌠 natum ⛓🚒 🏪🐹 labores ☔️ bibendum primis tristique 🕢⏲ ",
"images": [],
"likes": 901,
"replyCount": 239,
"messages": 6,
"comments": [
{
"id": "dfd9fd06-6f3e-4944-a129-6753353782be",
"userNickname": "@graece",
"userFullName": "Emmanuel Gilliam",
"userAvatar": "file:///android_asset/avatars/7.jpg",
"text": "😊 Haspostea dolore maiestatis adolescens dissentiunt tincidunt tale definiebas nisi referrentur tamquam pulvinar sociosqu ceteros cursus solum ultrices suas. Elaboraretdictas aliquam eius mnesarchum consetetur efficiantur graeco eam arcu mel duis unum accusata tation. Magnisviris quaerendum. Fugithonestatis a adipisci senserit singulis vidisse constituam principes. Doloremassa mea vulputate nonumes omnesque lorem accommodare signiferumque ancillae epicurei mediocrem labores intellegebat solet. ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174153,
"reply": null,
"poll": null
},
{
"id": "640d88a8-98ed-45de-9e82-0f3d26bb6fbe",
"userNickname": "@sociis",
"userFullName": "Eleanor Rowe",
"userAvatar": "file:///android_asset/avatars/78.jpg",
"text": "aliquid voluptatum",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974153,
"reply": null,
"poll": null
},
{
"id": "0d90f3ed-0171-4656-9a0e-51cc73f80ab1",
"userNickname": "@dicat",
"userFullName": "Earline Callahan",
"userAvatar": "file:///android_asset/avatars/25.jpg",
"text": "Appareatmentitum sagittis partiendo voluptatum epicuri brute debet velit consequat penatibus expetenda graeci sanctus legere posse vulputate inimicus noster. Egestascausae offendit dissentiunt lacus posse movet mnesarchum urna velit donec mucius lorem periculis dicta vivendo.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374153,
"reply": null,
"poll": null
},
{
"id": "fd875296-0b70-49b9-b078-ccb92e00be7f",
"userNickname": "@nonume",
"userFullName": "Kristen Booker",
"userAvatar": "file:///android_asset/avatars/31.jpg",
"text": "🎎🍓\nvocibus tibique🚓 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774153,
"reply": null,
"poll": null
},
{
"id": "8201c846-e329-4c82-b2f1-57e9f7fede3a",
"userNickname": "@porta",
"userFullName": "Aisha Banks",
"userAvatar": "file:///android_asset/avatars/17.jpg",
"text": "🦒🍣 minim 🖌🎶 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174153,
"reply": null,
"poll": null
},
{
"id": "8685d0ed-6bba-4f72-8974-04afbd5ef32a",
"userNickname": "@sumo",
"userFullName": "Jack Fuentes",
"userAvatar": "file:///android_asset/avatars/198.jpg",
"text": "venenatis phasellus lacus \n↘️㊙️ elaboraret referrentur 🌕 👧✊ laudem \n🏜 💅🌌 aliquet 😉 🔔🍘 nonumes ⛽️ 🈺🐏 labores \n🐊🚲 🦒🤵 debet 🌿 explicari natoque viverra \n🛒🐸 libris magna cum \n🍠 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974153,
"reply": null,
"poll": null
}
],
"createdAt": 1636965174153,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "1e754094-65f0-4c37-ab82-ba63a6ebee5e",
"userNickname": "@latine",
"userFullName": "Antonia Church",
"userAvatar": "file:///android_asset/avatars/116.jpg",
"text": "populo quod aptent 🍀 ",
"images": [],
"likes": 46,
"replyCount": 194,
"messages": 0,
"comments": [],
"createdAt": 1636957974153,
"reply": {
"id": "7055de32-2d8a-4b99-99af-ed781ddc9366",
"userNickname": "@nonume",
"userFullName": "Wendy Sutton",
"userAvatar": "file:///android_asset/avatars/95.jpg",
"text": "🏟🛵🕌 Quireque ocurreret quem urbanitas feugait libris mentitum reprehendunt simul dico. Adolescenslitora ferri dignissim senserit. Nullamadversarium inciderint ancillae reformidans gravida facilisis iusto efficitur ponderum tincidunt ut. Integerantiopam adolescens commune tempor facilisi neglegentur senectus ludus iaculis utinam ocurreret. Fuissetdefiniebas penatibus consectetuer et mandamus suavitate eirmod laudem ac has hac. Convallisquo libris mentitum putent splendide voluptaria ferri dis accusata nulla tritani molestiae primis vocibus integer saepe. ",
"images": [
"file:///android_asset/post_images/58.jpg"
],
"likes": 48,
"replyCount": 250,
"messages": 3,
"comments": [
{
"id": "9b1b7c53-d7f9-44cc-98e8-385c96d3850a",
"userNickname": "@dicit",
"userFullName": "Seymour Peck",
"userAvatar": "file:///android_asset/avatars/36.jpg",
"text": "interesset inimicus volumus \n😓👨🎤 ⏰🤶 elitr \n🔥🛢 signiferumque cetero platea \n🚒‼️ 🚄🍝 utinam 🐕 rutrum lacus ius 🚔 bibendum tation 🥢🌘 mus maluisset quas 🍦🤑 💏📅 morbi 🙌 🚏🚕 praesent \n🍺👩❤️👩 🍝👨🔬 suas 🌶🤵 🖼🙂 ubique 🆓🍭 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636932774153,
"reply": null,
"poll": null
},
{
"id": "6d033cfd-262b-42f3-95b4-092d4a343375",
"userNickname": "@placer",
"userFullName": "Casey Frazier",
"userAvatar": "file:///android_asset/avatars/176.jpg",
"text": "eum",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174153,
"reply": null,
"poll": null
},
{
"id": "41043a62-42bc-45d7-9e1b-03fc554af3e3",
"userNickname": "@dignis",
"userFullName": "Collin Walsh",
"userAvatar": "file:///android_asset/avatars/166.jpg",
"text": "🚜⛵️🌳 Erosetiam evertitur ad viris rhoncus ius electram wisi pri errem velit ad sapientem parturient. Dolorumpostulant quem ius inani ius sem. Periculafabellas mediocritatem cetero convenire euismod minim mea possim omnesque agam ancillae tristique honestatis noster no utinam mediocritatem quaerendum sonet.🍥🔦🕔\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174153,
"reply": null,
"poll": null
}
],
"createdAt": 1636950774153,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "3fc364bf-9045-4c08-945f-de0a5e6209da",
"userNickname": "@tantas",
"userFullName": "Roy Fletcher",
"userAvatar": "file:///android_asset/avatars/187.jpg",
"text": "vituperata omittantur assueverit 📈 🀄️🍴 ad ↩️ tempor neglegentur ⚰️ 🐙🎎 ligula 💢🚖 etiam malorum repudiare \n⤵️ ",
"images": [],
"likes": 888,
"replyCount": 39,
"messages": 9,
"comments": [
{
"id": "bf612145-c66f-4630-b68d-06f585185cac",
"userNickname": "@condim",
"userFullName": "Dominic Craft",
"userAvatar": "file:///android_asset/avatars/123.jpg",
"text": "🐢🤳🤔\nFusceerat utinam ferri morbi invidunt vero aliquet mollis vituperata pellentesque ad ligula orci consectetur pulvinar. Delectuscontentiones natum.📍🐪\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774153,
"reply": null,
"poll": null
},
{
"id": "8de1df99-5730-4931-86f2-e72f552383c7",
"userNickname": "@consti",
"userFullName": "Lazaro Sims",
"userAvatar": "file:///android_asset/avatars/64.jpg",
"text": "pro dictas mi",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174154,
"reply": null,
"poll": null
},
{
"id": "a821bda2-cd7e-4e37-90ab-cad6a7d7128b",
"userNickname": "@doloru",
"userFullName": "Truman Kane",
"userAvatar": "file:///android_asset/avatars/13.jpg",
"text": "alia iudicabit graeci",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636954374154,
"reply": null,
"poll": null
},
{
"id": "a7531f08-0782-4e21-91a4-1949d123e695",
"userNickname": "@montes",
"userFullName": "Marianne Hammond",
"userAvatar": "file:///android_asset/avatars/89.jpg",
"text": "mollis contentiones aperiri",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374154,
"reply": null,
"poll": null
},
{
"id": "7d943997-6cc2-4587-9a8b-6083b83842c0",
"userNickname": "@adipis",
"userFullName": "Vivian Fulton",
"userAvatar": "file:///android_asset/avatars/85.jpg",
"text": "Dignissimeruditi pertinacia natoque sit. Principesdicam elaboraret vulputate deseruisse cursus molestiae hinc fuisset oratio bibendum consequat ubique pertinacia possit graeco laudem habitant sonet ei. Fallihabeo suscipit vero error habemus ancillae similique lacinia ancillae. Definitionesinvenire vero tractatos.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774154,
"reply": null,
"poll": null
},
{
"id": "3d02e7fa-ed66-4b50-9238-b212da493132",
"userNickname": "@eirmod",
"userFullName": "Roderick Bray",
"userAvatar": "file:///android_asset/avatars/4.jpg",
"text": "Domingsenserit consetetur in pulvinar suscipit. Decoresenectus unum saperet assueverit solum invenire euripidis brute referrentur inceptos. Verituslaoreet definitiones posidonium delenit purus expetenda. Soletlorem reformidans erroribus omnesque venenatis. Sodaleslaudem luptatum eam hinc referrentur mus corrumpit persecuti no fabulas malorum lectus doctus pri errem.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374154,
"reply": null,
"poll": null
},
{
"id": "695d944a-7298-45df-9fbb-e884fcc4bf97",
"userNickname": "@eloque",
"userFullName": "Lewis Mitchell",
"userAvatar": "file:///android_asset/avatars/191.jpg",
"text": "doctus",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774154,
"reply": null,
"poll": null
},
{
"id": "a29bb97a-190c-44e9-a356-b46e198780df",
"userNickname": "@qui",
"userFullName": "Riley McNeil",
"userAvatar": "file:///android_asset/avatars/161.jpg",
"text": "Expetendapetentium ante erat ac laoreet feugait vis inani curae maiestatis ex justo. Communevocent oratio maiorum sagittis lacinia. Muciusaliquet dicit pertinacia quaestio ante turpis.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174154,
"reply": null,
"poll": null
},
{
"id": "f1640728-949f-481d-a2b7-623eee40a5bc",
"userNickname": "@deleni",
"userFullName": "Cecelia Keller",
"userAvatar": "file:///android_asset/avatars/167.jpg",
"text": "🌟🛋 ius \n🎁🔢 🚔🛒 volumus 👨🎓 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374154,
"reply": null,
"poll": null
}
],
"createdAt": 1636968774153,
"reply": null,
"poll": null
},
{
"id": "f0f03bf4-0438-43cb-9c8e-587845078956",
"userNickname": "@facili",
"userFullName": "Annie Frank",
"userAvatar": "file:///android_asset/avatars/184.jpg",
"text": "Evertiturquam erat class at quam detracto petentium habeo graece singulis. Electramnihil fuisset vitae atomorum habitasse diam mi deseruisse suspendisse platonem labores. Venenatisscripta nonumes causae tempor doming. Aliquetblandit interesset ea utamur tota lectus habeo. Graecenonumy vocibus nibh. Quodmediocrem maximus lorem mucius.",
"images": [],
"likes": 547,
"replyCount": 652,
"messages": 9,
"comments": [
{
"id": "09c59ad2-c23b-4aa7-b077-058bb283c088",
"userNickname": "@perpet",
"userFullName": "Magdalena Horton",
"userAvatar": "file:///android_asset/avatars/132.jpg",
"text": "Loremmnesarchum persius iriure congue erat altera sea electram inciderint magnis nominavi neque autem risus numquam fames feugait melius veri. Sociosqudis mattis recteque mucius libris ius iudicabit eget aliquid interesset percipit. Temporaffert magnis tation ipsum sollicitudin conceptam solum quaeque. Sanctushabitasse sanctus constituam indoctum numquam posse vehicula nam mazim tritani praesent necessitatibus nostra maiorum quaerendum erroribus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636936374154,
"reply": null,
"poll": null
},
{
"id": "7857f69a-8e1c-430e-9f0c-4f80f14733ca",
"userNickname": "@idque",
"userFullName": "Ray McMahon",
"userAvatar": "file:///android_asset/avatars/140.jpg",
"text": "Porttitorridiculus possim doctus adolescens neglegentur verterem mandamus his dicant molestie curae erroribus viris graeco eius dicam quaestio phasellus. Vituperataconceptam nisl ferri massa lacinia massa pertinax equidem iusto adolescens tale principes dicunt dicant lobortis non sanctus sollicitudin cubilia. Pellentesquevoluptaria dolor signiferumque. Mediocremluptatum class has dictum risus sale dissentiunt similique mazim nullam facilisis signiferumque option.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574154,
"reply": null,
"poll": null
},
{
"id": "73f69612-2e68-4384-a36b-3429b586b8a8",
"userNickname": "@prompt",
"userFullName": "Kelly Mitchell",
"userAvatar": "file:///android_asset/avatars/110.jpg",
"text": "Volumussapientem congue eius evertitur integer. Felisancillae causae vituperata graeco antiopam oratio vis morbi legere invidunt utroque cursus magna. Urnarepudiare feugait expetenda faucibus scripserit nisi persecuti sumo tota molestie malesuada. Evertiturconsetetur ancillae iisque noluisse labores accumsan pericula curabitur cras contentiones parturient nibh leo honestatis. Posidoniumauctor quis ea definitionem.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974154,
"reply": null,
"poll": null
},
{
"id": "e48656da-966d-411d-be1f-1c0663236f05",
"userNickname": "@graeci",
"userFullName": "Judy Edwards",
"userAvatar": "file:///android_asset/avatars/92.jpg",
"text": "Soletex appetere patrioque qui suas fusce eirmod scripserit sodales facilisis. Adversariumsale ad interpretaris elementum splendide penatibus possim nonumy oratio nascetur deterruisset conclusionemque. Sententiaeurbanitas vel porro quaerendum gubergren consequat augue id varius. Esseverterem tincidunt simul reformidans dico. Docendimi nominavi quis quod mei.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574154,
"reply": null,
"poll": null
},
{
"id": "34fe7ce2-9edb-4b73-98f8-9dae4cfa6fc5",
"userNickname": "@errori",
"userFullName": "Roy Jennings",
"userAvatar": "file:///android_asset/avatars/175.jpg",
"text": "😍🍿👩❤️👩 ornatus alia docendi dissentiunt💯🐁 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974154,
"reply": null,
"poll": null
},
{
"id": "938e14f1-87f5-4d64-8d13-debcf849b0ee",
"userNickname": "@dignis",
"userFullName": "Maryann York",
"userAvatar": "file:///android_asset/avatars/38.jpg",
"text": "🗜🛤 suscipiantur 🎚🥐 himenaeos ornare 💡 has viverra elaboraret 🚍 persequeris ancillae ⏲🍣 convallis graeco petentium \n⚒ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774154,
"reply": null,
"poll": null
},
{
"id": "4594d5da-acb2-4a93-a7b4-9f4bd77bfe1d",
"userNickname": "@mea",
"userFullName": "Daren Webster",
"userAvatar": "file:///android_asset/avatars/105.jpg",
"text": "legimus singulis fusce fabulas",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974154,
"reply": null,
"poll": null
},
{
"id": "bf584316-adab-4ee4-b338-8fe92ed67ee7",
"userNickname": "@habita",
"userFullName": "Shelby Ashley",
"userAvatar": "file:///android_asset/avatars/128.jpg",
"text": "Ceterosaliquip an consul aliquam causae sociosqu ponderum petentium phasellus sed has alterum liber. Disceredoming amet lectus repudiandae natum cum pri.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636929174154,
"reply": null,
"poll": null
},
{
"id": "51d82112-f1a1-4a21-b0aa-b8da31a807be",
"userNickname": "@viverr",
"userFullName": "Evan Estrada",
"userAvatar": "file:///android_asset/avatars/179.jpg",
"text": "⏰🍒\nexplicari deseruisse laudem saperet pertinacia6️⃣🦋📗\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636950774154,
"reply": null,
"poll": null
}
],
"createdAt": 1636968774154,
"reply": null,
"poll": null
},
{
"id": "b08c5820-3d48-4cfd-a754-a5f46eb4dcfd",
"userNickname": "@erat",
"userFullName": "Cheri Porter",
"userAvatar": "file:///android_asset/avatars/86.jpg",
"text": "posuere et homero",
"images": [],
"likes": 94,
"replyCount": 137,
"messages": 0,
"comments": [],
"createdAt": 1636932774154,
"reply": {
"id": "1235d02b-648b-4477-9420-9dc9eff59ae1",
"userNickname": "@ei",
"userFullName": "Jaime Heath",
"userAvatar": "file:///android_asset/avatars/162.jpg",
"text": "🚧\nlaudem dolor🥥\n",
"images": [],
"likes": 821,
"replyCount": 312,
"messages": 4,
"comments": [
{
"id": "0e6fb3d1-a0a6-410a-95c0-55b130aeea8a",
"userNickname": "@mel",
"userFullName": "Kellie Chase",
"userAvatar": "file:///android_asset/avatars/65.jpg",
"text": "🐖🗼👀\nmoderatius hac tortor platea ad🥙🥗 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974154,
"reply": null,
"poll": null
},
{
"id": "2a81024e-be78-46b3-b530-ba3692bb37ab",
"userNickname": "@solum",
"userFullName": "Maryellen Marshall",
"userAvatar": "file:///android_asset/avatars/40.jpg",
"text": "🕋👲\nQuotpetentium nihil contentiones vehicula posse vim eripuit utroque feugait vitae viverra suspendisse mazim duo iudicabit utamur. Dissentiunttaciti reprehendunt risus ornare similique.🚳🌖🧥\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374154,
"reply": null,
"poll": null
},
{
"id": "3985940a-0bfb-42cd-979b-10247ebbf1eb",
"userNickname": "@accusa",
"userFullName": "Socorro Sellers",
"userAvatar": "file:///android_asset/avatars/81.jpg",
"text": "erroribus no 🍇🤷♀ ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574154,
"reply": null,
"poll": null
},
{
"id": "7cb39020-15b5-42ef-97f1-476882090e18",
"userNickname": "@dolor",
"userFullName": "Cruz Chan",
"userAvatar": "file:///android_asset/avatars/2.jpg",
"text": "🗒㊗️🌚\noporteat tractatos🍬😼🅾️\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637001174154,
"reply": null,
"poll": null
}
],
"createdAt": 1636932774154,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 936,
"positions": [
{
"text": "docendi delicata fringilla",
"voted": 326
},
{
"text": "saperet consul hac",
"voted": 405
},
{
"text": "nulla prompta",
"voted": 205
}
]
}
},
"poll": null
},
{
"id": "ffa9f22b-c2bb-4576-84b2-c061b32daaa0",
"userNickname": "@indoct",
"userFullName": "Stewart Boyer",
"userAvatar": "file:///android_asset/avatars/136.jpg",
"text": "graeco maecenas facilis verear",
"images": [],
"likes": 78,
"replyCount": 50,
"messages": 0,
"comments": [],
"createdAt": 1637008374154,
"reply": {
"id": "da9348d5-9c22-4b83-b2d9-5cf898aeb739",
"userNickname": "@maluis",
"userFullName": "Scott Russo",
"userAvatar": "file:///android_asset/avatars/71.jpg",
"text": "Feugaitludus graeci adolescens numquam dicit liber repudiandae voluptatum platea dicunt gubergren neque omittam iudicabit tritani urna scelerisque suspendisse inceptos. Diamaltera tincidunt interdum contentiones mediocritatem eripuit. Verisimilique scripserit. Harumpossit ne ancillae sale sed quod principes necessitatibus ornare dissentiunt utroque errem appetere ipsum autem pulvinar scelerisque prompta. Hasexpetenda imperdiet invenire corrumpit recteque. Instructiorleo interpretaris vituperata dicant solet postulant consectetuer.",
"images": [
"file:///android_asset/post_images/68.jpg",
"file:///android_asset/post_images/24.jpg"
],
"likes": 422,
"replyCount": 275,
"messages": 8,
"comments": [
{
"id": "aee2d5a0-a4e6-42ee-a772-161ae6ed1961",
"userNickname": "@medioc",
"userFullName": "Seth Mitchell",
"userAvatar": "file:///android_asset/avatars/126.jpg",
"text": "Habeovoluptatibus dictum. Dictumstluptatum natum porta dicam elitr epicuri habitant melius elitr esse consequat. Antecontentiones discere causae ferri natum instructior dignissim gubergren dicam arcu.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974155,
"reply": null,
"poll": null
},
{
"id": "f9800b3f-9c61-4949-a688-6804eb534460",
"userNickname": "@detrax",
"userFullName": "Kate Williams",
"userAvatar": "file:///android_asset/avatars/112.jpg",
"text": "🖐🎠🍰 Felisgraece turpis aliquet postea omnesque populo ipsum doctus eros fastidii quaerendum fringilla alterum. Eloquentiameloquentiam unum ornatus aliquam eruditi erroribus eu taciti eget odio aliquid tellus convenire invidunt constituam.🚶♀\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374155,
"reply": null,
"poll": null
},
{
"id": "c699f155-3330-4e65-b7e0-9f22fd92b214",
"userNickname": "@nascet",
"userFullName": "Isaac Perry",
"userAvatar": "file:///android_asset/avatars/18.jpg",
"text": "💴\nreque evertitur per repudiare iriure🌘 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774155,
"reply": null,
"poll": null
},
{
"id": "77812b8f-e877-40df-bdcb-36fdbdf4d183",
"userNickname": "@himena",
"userFullName": "Armando Valenzuela",
"userAvatar": "file:///android_asset/avatars/86.jpg",
"text": "a faucibus",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636943574155,
"reply": null,
"poll": null
},
{
"id": "070ea41d-d3c9-46d5-8beb-a805b155722e",
"userNickname": "@finibu",
"userFullName": "Deanne Carr",
"userAvatar": "file:///android_asset/avatars/139.jpg",
"text": "Laudemfusce ei assueverit ludus moderatius habemus cursus dictum latine vehicula epicuri. Exqualisque proin scripserit varius quem cu hac et semper alia impetus partiendo hinc dictas. Delenitsalutatus class homero enim senserit facilis hinc honestatis eleifend malesuada ornatus evertitur utroque tacimates. Reformidanssolet salutatus propriae condimentum iisque efficiantur aperiri posse discere viris autem. Eloquentiamporttitor nonumy his massa ius constituto consul tempor commodo posidonium qualisque tempor iriure dolores dis eros nec bibendum. Vimcommune evertitur vestibulum torquent dicam fringilla malorum erat varius metus quot adipiscing in laudem diam feugiat suscipiantur cursus laudem.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974155,
"reply": null,
"poll": null
},
{
"id": "4034c3bd-6980-4487-a4e8-3e1c331f309e",
"userNickname": "@dis",
"userFullName": "Bianca Carney",
"userAvatar": "file:///android_asset/avatars/150.jpg",
"text": "🏮🛋\nat✨🚎🐤\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774155,
"reply": null,
"poll": null
},
{
"id": "6dc799cc-9c05-46a9-bd48-123211c5cbb3",
"userNickname": "@his",
"userFullName": "Lionel McIntosh",
"userAvatar": "file:///android_asset/avatars/186.jpg",
"text": "♊️\nputent ei quo qualisque gubergren🔟🕟\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974155,
"reply": null,
"poll": null
},
{
"id": "047f8c4e-ee93-460a-9f3f-1b63314f066e",
"userNickname": "@ponder",
"userFullName": "Kristina Keller",
"userAvatar": "file:///android_asset/avatars/2.jpg",
"text": "📍🉐🕥\nAppetereinteresset dissentiunt solet velit sumo salutatus aliquip vestibulum solum sapien semper pharetra reprehendunt. Praesentantiopam omittam neque sociosqu splendide congue posse vero accumsan ultricies fames dolor bibendum nibh viverra tacimates definitiones interdum. Latineminim accumsan intellegat feugiat vocibus ante interpretaris quo inceptos omittam. Nosterno efficiantur feugiat regione conclusionemque.\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636939974155,
"reply": null,
"poll": null
}
],
"createdAt": 1636993974154,
"reply": null,
"poll": null
},
"poll": null
},
{
"id": "5a0d9cab-1b8c-48b0-96c7-7e662d71b3e7",
"userNickname": "@sapien",
"userFullName": "Maynard Hull",
"userAvatar": "file:///android_asset/avatars/196.jpg",
"text": "Adlacus gloriatur. Ligulasalutatus reque quod omnesque eruditi.",
"images": [
"file:///android_asset/post_images/61.jpg",
"file:///android_asset/post_images/57.jpg"
],
"likes": 394,
"replyCount": 392,
"messages": 2,
"comments": [
{
"id": "f965a3d3-2bb1-4338-bfb7-60974dd824a0",
"userNickname": "@posuer",
"userFullName": "Gretchen Woodward",
"userAvatar": "file:///android_asset/avatars/27.jpg",
"text": "🚕🚞 sumo 🛶 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574155,
"reply": null,
"poll": null
},
{
"id": "734a9ddc-5ade-4bbd-9422-513a7298fc37",
"userNickname": "@ludus",
"userFullName": "Roger Carver",
"userAvatar": "file:///android_asset/avatars/23.jpg",
"text": "Habitasseputent ultrices suavitate maximus imperdiet signiferumque omnesque tation elitr affert mauris suscipit brute deseruisse conclusionemque erat pretium. Fabellaspulvinar legimus moderatius curabitur saperet suscipiantur quis fuisset has platea dui. Porropertinacia veniam viderer sapien ancillae persecuti sed errem civibus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574155,
"reply": null,
"poll": null
}
],
"createdAt": 1637001174155,
"reply": null,
"poll": null
},
{
"id": "701a8fdb-dbe9-4b9e-9d97-0d1e5c80b252",
"userNickname": "@duo",
"userFullName": "Donna Potter",
"userAvatar": "file:///android_asset/avatars/77.jpg",
"text": "Queminimicus reque minim ornatus praesent suscipit aperiri conceptam habitant imperdiet sapientem molestie periculis quas eu deseruisse nonumes. Diamconsectetur libero.",
"images": [],
"likes": 615,
"replyCount": 414,
"messages": 3,
"comments": [
{
"id": "a4144cb4-e39b-40b2-9fba-06d6df067332",
"userNickname": "@omitta",
"userFullName": "Blair Shields",
"userAvatar": "file:///android_asset/avatars/9.jpg",
"text": "🍐 pri🍙\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636975974155,
"reply": null,
"poll": null
},
{
"id": "62db7b25-91c7-46be-809f-200b080518d2",
"userNickname": "@quas",
"userFullName": "Bridget Ashley",
"userAvatar": "file:///android_asset/avatars/155.jpg",
"text": "💳☎️ pellentesque \n📳😂 🔗📯 nam \n🚶🍩 adhuc omittantur \n🕤 ubique vocibus \n🎇 velit scripserit ponderum 1️⃣😃 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574155,
"reply": null,
"poll": null
},
{
"id": "26cc5050-ede5-4611-9ff4-399ed879550e",
"userNickname": "@nihil",
"userFullName": "Estelle Cain",
"userAvatar": "file:///android_asset/avatars/71.jpg",
"text": "👻🤥 Litoramovet vim rhoncus vocibus quod rhoncus quam ponderum phasellus mutat ubique doctus. Sententiaenatoque utroque. Vistempus dictumst odio qualisque minim eius eam posuere voluptatibus primis. Penatibusnatum pro natum felis mutat affert dicant felis lobortis nonumy splendide augue lorem eloquentiam. Habitantponderum pulvinar. Invenireiaculis detracto.🌮🥧 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574155,
"reply": null,
"poll": null
}
],
"createdAt": 1636950774155,
"reply": null,
"poll": null
},
{
"id": "b7dbfdfa-fc10-4818-b608-b308f7d716ef",
"userNickname": "@sapien",
"userFullName": "Erika Pittman",
"userAvatar": "file:///android_asset/avatars/166.jpg",
"text": "an falli",
"images": [],
"likes": 22,
"replyCount": 141,
"messages": 0,
"comments": [],
"createdAt": 1636990374155,
"reply": {
"id": "f4c71112-04f0-45a7-9024-8a32fff3127a",
"userNickname": "@medioc",
"userFullName": "Salvador Duran",
"userAvatar": "file:///android_asset/avatars/60.jpg",
"text": "option adolescens vidisse tortor at",
"images": [],
"likes": 263,
"replyCount": 457,
"messages": 1,
"comments": [
{
"id": "b3040d5c-0b22-4a6e-86ab-5814128ce159",
"userNickname": "@id",
"userFullName": "Johnathon Battle",
"userAvatar": "file:///android_asset/avatars/95.jpg",
"text": "💸😄 Fabulasvarius phasellus expetendis mutat. Torquentpatrioque montes appareat mel commodo dicat sagittis. Pertinaciaferri nihil altera imperdiet posuere himenaeos periculis nostrum moderatius purus. Dicamnisi cubilia nullam harum et eu vero tota repudiare quod.⏳🌥🆙 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637004774155,
"reply": null,
"poll": null
}
],
"createdAt": 1636986774155,
"reply": null,
"poll": {
"isCompleted": true,
"totalVotes": 1426,
"positions": [
{
"text": "suavitate tempus enim ante mea",
"voted": 472
},
{
"text": "integer nonumy",
"voted": 160
},
{
"text": "fugit vivamus aliquid pretium",
"voted": 398
},
{
"text": "tritani",
"voted": 396
}
]
}
},
"poll": null
},
{
"id": "aee77efc-e25e-4a2e-a90f-44d452bf6854",
"userNickname": "@reform",
"userFullName": "Cyril Gamble",
"userAvatar": "file:///android_asset/avatars/118.jpg",
"text": "Nasceturlorem dicam explicari conubia possim latine fastidii amet cu noster feugait quaeque brute aenean molestiae tantas iudicabit intellegat. Orciveritus noster corrumpit nec sale errem senserit lorem duo esse conceptam. Latineadversarium quod repudiare utinam liber error neglegentur nostrum ultricies quaeque. Rutrummutat proin iudicabit felis option interesset enim suscipit mnesarchum convallis euismod corrumpit usu pri placerat. Sollicitudinquot falli aenean moderatius.",
"images": [],
"likes": 505,
"replyCount": 332,
"messages": 6,
"comments": [
{
"id": "21d04a7d-c8ea-48f4-b92d-2a484a76d771",
"userNickname": "@saluta",
"userFullName": "Jessie Howard",
"userAvatar": "file:///android_asset/avatars/5.jpg",
"text": "Alteraduis wisi posidonium tota curabitur nonumes est audire velit. Variusdis liber appareat labores dui alia aliquet litora patrioque lacus nonumy mutat verear erroribus eleifend. Torquentusu purus atomorum oratio varius iudicabit principes netus quidam suspendisse sea dicta eripuit. Facilisistincidunt diam noster tibique repudiandae homero inani offendit pertinacia utamur vitae fringilla necessitatibus vivendo lobortis. Quisaperiri dolorem nullam laudem tamquam elit.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574155,
"reply": null,
"poll": null
},
{
"id": "c78bae6a-dc56-4fb3-9ee0-9cf47759c780",
"userNickname": "@cetero",
"userFullName": "Helen Maddox",
"userAvatar": "file:///android_asset/avatars/72.jpg",
"text": "Malorumlaudem sea indoctum interesset mazim. Vulputatefacilis pro tractatos unum expetenda eum varius.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636957974155,
"reply": null,
"poll": null
},
{
"id": "c579d5ca-fa97-46d8-9f9d-ee72d145db01",
"userNickname": "@eloque",
"userFullName": "Nola Townsend",
"userAvatar": "file:///android_asset/avatars/154.jpg",
"text": "Prosalutatus definitionem dicat maiorum prodesset omittantur te netus maecenas delectus gubergren. Nullasociosqu epicuri vim lacinia ea vis eloquentiam cubilia. Malesuadafugit constituto.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636983174155,
"reply": null,
"poll": null
},
{
"id": "f85c8a9c-84a0-43f2-b5e7-72fccfcf8dd2",
"userNickname": "@idque",
"userFullName": "Ignacio Maldonado",
"userAvatar": "file:///android_asset/avatars/23.jpg",
"text": "dignissim urna finibus metus",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636968774155,
"reply": null,
"poll": null
},
{
"id": "c71ce725-4f4c-46df-b928-4f3f09d332dc",
"userNickname": "@possit",
"userFullName": "Billie Lynn",
"userAvatar": "file:///android_asset/avatars/1.jpg",
"text": "signiferumque lorem possim 👩👩👧👧 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574156,
"reply": null,
"poll": null
},
{
"id": "b011c7d6-b7ec-459d-b54f-8d249f356ce3",
"userNickname": "@veniam",
"userFullName": "Patricia Blackburn",
"userAvatar": "file:///android_asset/avatars/148.jpg",
"text": "🍸\nAdipisciad veniam definitionem veniam veniam orci atomorum consul sententiae montes condimentum. Tibiqueiusto cras sapientem phasellus sanctus. Facilisiregione velit. Magnanovum vituperata posuere reformidans litora hinc. Alteruminteresset vix consequat pericula malesuada wisi ne nullam nisl pri vestibulum conubia. Doloremmaiorum mi ei quod feugiat tritani non lorem eleifend.🍗👨👩👧👦 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574156,
"reply": null,
"poll": null
}
],
"createdAt": 1636929174155,
"reply": null,
"poll": null
},
{
"id": "59ded396-4178-41b7-9194-d3b2080d5f22",
"userNickname": "@commod",
"userFullName": "Cleveland Curry",
"userAvatar": "file:///android_asset/avatars/150.jpg",
"text": "Eahabeo leo ceteros tale unum eros montes turpis definitiones habemus quot. Salutatusiriure graeci quaerendum consectetuer ocurreret voluptatibus option. Accusataipsum sententiae dico.",
"images": [],
"likes": 377,
"replyCount": 437,
"messages": 9,
"comments": [
{
"id": "4abc0e64-8741-4c6e-bdb3-104b417e6bcb",
"userNickname": "@luptat",
"userFullName": "David Wade",
"userAvatar": "file:///android_asset/avatars/153.jpg",
"text": "🏦\nconceptam laoreet persecuti lacus🐨👨👨👧🥧\n",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636979574156,
"reply": null,
"poll": null
},
{
"id": "e7f31449-ae76-4c39-abd0-12ad8ffe08b1",
"userNickname": "@tortor",
"userFullName": "Dale Estrada",
"userAvatar": "file:///android_asset/avatars/109.jpg",
"text": "☕️4️⃣📣 alienum finibus vidisse scripserit doctus ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636986774156,
"reply": null,
"poll": null
},
{
"id": "09e6d523-9923-4e5c-b040-f4a5a30a78cd",
"userNickname": "@reprim",
"userFullName": "Darius Gibbs",
"userAvatar": "file:///android_asset/avatars/0.jpg",
"text": "🐎 ridens tractatos graeco♊️📡🥫 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636993974156,
"reply": null,
"poll": null
},
{
"id": "b6a01426-5178-409c-926f-1811f836ee56",
"userNickname": "@vehicu",
"userFullName": "Emile Palmer",
"userAvatar": "file:///android_asset/avatars/48.jpg",
"text": "Putentcongue doming reformidans numquam reprimique ea dicam voluptatum fabellas dignissim oratio impetus pro salutatus tractatos mutat nominavi scelerisque ridiculus. Turpiserror vel sem facilis malorum persecuti ac theophrastus adipisci. Aconstituam est aliquam vel varius cubilia verear integer nibh eum conubia rhoncus voluptatibus principes blandit vocent dicit est. Definitionemvel delenit nisl meliore congue dicta theophrastus.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636961574156,
"reply": null,
"poll": null
},
{
"id": "a797f072-954d-4299-9580-be2869258c3d",
"userNickname": "@augue",
"userFullName": "Janna Decker",
"userAvatar": "file:///android_asset/avatars/96.jpg",
"text": "📱🎍 harum \n🗺 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636997574156,
"reply": null,
"poll": null
},
{
"id": "f08e7b8d-3cc9-471d-af3b-cd235a4e076c",
"userNickname": "@persec",
"userFullName": "Sheila Boyle",
"userAvatar": "file:///android_asset/avatars/75.jpg",
"text": "ridiculus inimicus reque 👨🍳 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636947174156,
"reply": null,
"poll": null
},
{
"id": "b2218a4c-c297-486d-9ebd-87206b0df1d3",
"userNickname": "@posse",
"userFullName": "Virgie Vasquez",
"userAvatar": "file:///android_asset/avatars/193.jpg",
"text": "Agamfacilis fuisset aliquid pharetra suspendisse gloriatur nullam aeque tation option persequeris quem vero curae ipsum mutat. Ornarenatoque maecenas urna fabulas eros dis graecis oporteat vitae conceptam laudem iaculis pertinacia maiorum tincidunt deseruisse. Liberridens graeco petentium. Novumpostea invenire agam velit dicit. Volumusdictas vestibulum mei.",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636990374156,
"reply": null,
"poll": null
},
{
"id": "77cd8854-2e81-434d-86fb-a8df8ff840c7",
"userNickname": "@oporte",
"userFullName": "Alfred Lewis",
"userAvatar": "file:///android_asset/avatars/60.jpg",
"text": "🍒⚡️ homero 🐔 ",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1637011974156,
"reply": null,
"poll": null
},
{
"id": "765e9551-d907-41d3-b726-060be593f762",
"userNickname": "@defini",
"userFullName": "Donnie Olsen",
"userAvatar": "file:///android_asset/avatars/42.jpg",
"text": "bibendum felis netus curae",
"images": [],
"likes": 0,
"replyCount": 0,
"messages": 0,
"comments": [],
"createdAt": 1636965174156,
"reply": null,
"poll": null
}
],
"createdAt": 1636943574156,
"reply": null,
"poll": null
}
]
================================================
FILE: app/src/main/java/dev/serhiiyaremych/imla/DemoApp.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import dev.serhiiyaremych.imla.data.UserPost
import kotlinx.serialization.json.Json
class DemoApp : Application() {
override fun onCreate() {
super.onCreate()
ctx = this
}
companion object {
@SuppressLint("StaticFieldLeak")
private lateinit var ctx: Context
val posts: List by lazy {
val tweetsJson = ctx.assets.open("loremipsum.json").bufferedReader().readText()
Json.decodeFromString>(tweetsJson)
}
}
}
================================================
FILE: app/src/main/java/dev/serhiiyaremych/imla/MainActivity.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Choreographer
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.ReportDrawnWhen
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.safeGesturesPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.tracing.trace
import dev.serhiiyaremych.imla.data.ApiClient
import dev.serhiiyaremych.imla.modifier.blurSource
import dev.serhiiyaremych.imla.ui.BackdropBlur
import dev.serhiiyaremych.imla.ui.theme.ImlaTheme
import dev.serhiiyaremych.imla.ui.userpost.SimpleImageViewer
import dev.serhiiyaremych.imla.ui.userpost.UserPostView
import dev.serhiiyaremych.imla.uirenderer.Style
import dev.serhiiyaremych.imla.uirenderer.UiLayerRenderer
import dev.serhiiyaremych.imla.uirenderer.rememberUiLayerRenderer
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import java.util.Locale
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(
android.graphics.Color.TRANSPARENT,
),
navigationBarStyle = SystemBarStyle.light(
android.graphics.Color.TRANSPARENT, android.graphics.Color.TRANSPARENT
)
)
launchIdlenessTracking()
setContent {
ImlaTheme {
val uiRenderer = rememberUiLayerRenderer(downSampleFactor = 2)
val viewingImage = remember {
mutableStateOf("")
}
Box(modifier = Modifier.fillMaxSize()) {
// Full height content
Surface(
Modifier
.fillMaxSize()
.blurSource(uiRenderer),
) {
Content(modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(top = TopAppBarDefaults.MediumAppBarExpandedHeight),
onImageClick = { viewingImage.value = it },
onScroll = { /*uiRenderer.onUiLayerUpdated()*/ })
}
val showBottomSheet = remember { mutableStateOf(false) }
Column(modifier = Modifier.fillMaxSize()) {
// Layer 0 above full height content
// BlurryTopAppBar(uiRenderer)
// Layer 1 full height content
Spacer(Modifier.weight(1f))
// BackdropBlur(Modifier.requiredSize(120.dp), uiRenderer)
Spacer(Modifier.weight(1f))
Row {
BackdropBlur(Modifier.size(120.dp), uiRenderer)
BackdropBlur(Modifier.size(120.dp), uiRenderer)
BackdropBlur(Modifier.size(120.dp), uiRenderer)
}
Spacer(Modifier.weight(1f))
// BlurryBottomNavBar(uiRenderer) {
// showBottomSheet.value = true
// }
}
val cornerShape = RoundedCornerShape(12.dp)
// BackdropBlur(
// modifier = Modifier
// .fillMaxSize()
// .shadow(2.dp, cornerShape)
// .border(
// 1.dp, Color.Cyan.copy(alpha = 0.4f).compositeOver(Color.White),
// cornerShape
// )
// .align(Alignment.Center),
// rendererState = uiRenderer,
// blurMask = Brush.radialGradient(
// colors = listOf(
// Color.White.copy(alpha = 0.0f),
// Color.White.copy(alpha = 0.5f),
//// Color.White.copy(alpha = 0.9f),
//// Color.White.copy(alpha = 1.0f),
// Color.White.copy(alpha = 1.0f),
// ),
// ),
// clipShape = cornerShape
// )
AnimatedVisibility(
modifier = Modifier
.matchParentSize(),
visible = viewingImage.value.isNotEmpty(),
enter = fadeIn(),
exit = fadeOut()
) {
BackdropBlur(
modifier = Modifier.matchParentSize(),
rendererState = uiRenderer,
) {
SimpleImageViewer(
modifier = Modifier
.fillMaxSize(),
imageUrl = viewingImage.value,
onDismiss = { viewingImage.value = "" })
}
DisposableEffect(key1 = Unit) {
onDispose { uiRenderer.onUiLayerUpdated() }
}
}
if (showBottomSheet.value) {
val sheetState = rememberModalBottomSheetState()
val blurOpacity = remember {
mutableFloatStateOf(1f)
}
val sheetHeight = remember {
mutableIntStateOf(0)
}
val noiseState = remember {
mutableFloatStateOf(0.1f)
}
val offsetState = remember {
mutableFloatStateOf(0.8f)
}
val passesState = remember {
mutableFloatStateOf(1f)
}
val passes = (passesState.floatValue * (4 - 1) + 1).toInt().coerceIn(1, 4)
BackdropBlur(
modifier = Modifier
.fillMaxSize()
.zIndex(2f),
style = Style.default.copy(
noiseAlpha = noiseState.floatValue,
offset = offsetState.floatValue,
passes = passes,
blurOpacity = blurOpacity.floatValue
),
rendererState = uiRenderer
)
ModalBottomSheet(
modifier = Modifier
.statusBarsPadding()
.onSizeChanged { sheetHeight.intValue = it.height },
sheetState = sheetState,
scrimColor = Color.Transparent,
onDismissRequest = { showBottomSheet.value = false },
containerColor = Color.White.copy(alpha = (1.0f - blurOpacity.floatValue) + 0.3f)
) {
Column(
Modifier
.fillMaxSize()
.safeGesturesPadding(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Blur Settings", fontWeight = FontWeight.Bold)
Spacer(Modifier.height(16.dp))
Text(
modifier = Modifier.fillMaxWidth(),
text = "Blur opacity ${
String.format(
locale = Locale.ENGLISH,
"%.1f",
blurOpacity.floatValue
)
}"
)
Row(verticalAlignment = Alignment.CenterVertically) {
val noiseStr = String.format(
locale = Locale.ENGLISH,
format = "%.1f", noiseState.floatValue
)
Text("Noise($noiseStr)")
Spacer(Modifier.width(16.dp))
Slider(
value = noiseState.floatValue,
onValueChange = { noiseState.floatValue = it },
valueRange = 0.0f..1.0f
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
val offsetStr = String.format(
locale = Locale.ENGLISH,
format = "%.1f", offsetState.floatValue
)
Text("Offset($offsetStr)")
Spacer(Modifier.width(16.dp))
Slider(
value = offsetState.floatValue,
onValueChange = { offsetState.floatValue = it },
valueRange = 0.5f..2.2f
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Passes($passes)")
Spacer(Modifier.width(16.dp))
Slider(
value = passesState.floatValue,
onValueChange = { passesState.floatValue = it },
valueRange = 0f..1f,
steps = 2
)
}
}
}
LaunchedEffect(sheetState) {
snapshotFlow { sheetState.requireOffset() }
.distinctUntilChanged()
.collect {
val expandFraction = 1.0f - (it / sheetHeight.intValue)
blurOpacity.floatValue = expandFraction.coerceIn(0f, 1f)
}
}
}
}
val fullyDrawn = remember { mutableStateOf(false) }
LaunchedEffect(uiRenderer.isInitialized, fullyDrawn) {
snapshotFlow { uiRenderer.isInitialized.value }
.collect {
if (it) {
delay(1000)
fullyDrawn.value = true
}
}
}
ReportDrawnWhen { fullyDrawn.value }
}
}
}
@Composable
private fun Wrapped() {
AndroidView(
factory = { context ->
View(context).apply {
isFocusable = false
isFocusableInTouchMode = false
background =
ColorDrawable(Color.Blue.copy(alpha = 0.5f).toArgb())
}
},
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.5f)
.border(1.dp, Color.Magenta)
)
}
@Composable
private fun BlurryBottomNavBar(
uiRenderer: UiLayerRenderer,
onShowSettings: () -> Unit
) = Box(Modifier.height(86.dp)) {
BackdropBlur(
modifier = Modifier
.fillMaxWidth()
.border(
Dp.Hairline,
Color.DarkGray,
RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
),
rendererState = uiRenderer,
style = Style.default.copy(passes = 3, noiseAlpha = 0.1f, blurOpacity = 0.9f)
// blurMask = Brush.verticalGradient(
// colors = listOf(
// Color.White.copy(alpha = 0.0f),
// Color.White.copy(alpha = 0.6f),
// Color.White.copy(alpha = 0.9f),
// Color.White.copy(alpha = 1.0f),
// Color.White.copy(alpha = 1.0f),
// ),
// ),
) {
NavigationBar(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
windowInsets = WindowInsets(bottom = 0.dp),
containerColor = Color.Transparent
) {
NavigationBarItem(selected = false, onClick = { /*TODO*/ }, icon = {
Icon(
imageVector = Icons.Filled.Home, contentDescription = null
)
})
NavigationBarItem(selected = false, onClick = { /*TODO*/ }, icon = {
Icon(
imageVector = Icons.Filled.Search, contentDescription = null
)
})
NavigationBarItem(selected = false, onClick = { /*TODO*/ }, icon = {
Icon(
imageVector = Icons.Filled.Notifications, contentDescription = null
)
})
NavigationBarItem(selected = false, onClick = onShowSettings, icon = {
Icon(
imageVector = Icons.Filled.Settings, contentDescription = null
)
})
}
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun BlurryTopAppBar(uiRenderer: UiLayerRenderer) {
BackdropBlur(
modifier = Modifier.height(120.dp),
rendererState = uiRenderer,
style = Style.default.copy(passes = 3, noiseAlpha = 0.1f),
// blurMask = Brush.verticalGradient(
// colors = listOf(
// Color.White.copy(alpha = 1.0f),
// Color.White.copy(alpha = 1.0f),
// Color.White.copy(alpha = 0.9f),
// Color.White.copy(alpha = 0.5f),
// Color.White.copy(alpha = 0.0f),
// ),
// ),
) {
TopAppBar(
modifier = Modifier.statusBarsPadding(),
title = { Text("Blur Demo") },
windowInsets = WindowInsets(top = 0.dp),
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
navigationIcon = {
IconButton(onClick = { /* "Open nav drawer" */ }) {
Icon(Icons.Filled.Menu, contentDescription = null)
}
}
)
}
}
@Composable
private fun Content(
modifier: Modifier,
contentPadding: PaddingValues,
onImageClick: (String) -> Unit,
onScroll: (Int) -> Unit
) = trace("MainActivity#Content") {
val scrollState = rememberLazyListState()
val currentOnScroll = rememberUpdatedState(onScroll).value
LaunchedEffect(key1 = scrollState, key2 = onScroll) {
snapshotFlow { scrollState.firstVisibleItemScrollOffset }.distinctUntilChanged()
.collect {
currentOnScroll(it)
}
}
val posts =
ApiClient.getPosts().collectAsStateWithLifecycle(initialValue = persistentListOf())
LazyColumn(modifier = modifier, state = scrollState, contentPadding = contentPadding) {
items(posts.value, key = { it.id }) { item ->
UserPostView(
modifier = Modifier.fillMaxWidth(), post = item, onImageClick = onImageClick
)
}
}
}
private fun ComponentActivity.launchIdlenessTracking() {
val contentView: View = findViewById(android.R.id.content)
val callback: Choreographer.FrameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (Recomposer.runningRecomposers.value.any { it.hasPendingWork }) {
contentView.contentDescription = "COMPOSE-BUSY"
} else {
contentView.contentDescription = "COMPOSE-IDLE"
}
Choreographer.getInstance().postFrameCallback(this)
}
}
Choreographer.getInstance().postFrameCallback(callback)
}
}
================================================
FILE: app/src/main/java/dev/serhiiyaremych/imla/data/ApiClient.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.data
import dev.serhiiyaremych.imla.DemoApp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
object ApiClient {
fun getPosts(): Flow> {
return flow {
val posts = withContext(Dispatchers.IO) {
DemoApp.posts.toPersistentList()
}
emit(posts)
}
}
}
================================================
FILE: app/src/main/java/dev/serhiiyaremych/imla/data/Poll.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.data
import androidx.compose.runtime.Immutable
import kotlinx.serialization.Serializable
@Serializable
@Immutable
data class Poll(
val isCompleted: Boolean,
val totalVotes: Int,
val positions: List
)
@Serializable
@Immutable
data class PollPosition(
val text: String,
val voted: Int
)
================================================
FILE: app/src/main/java/dev/serhiiyaremych/imla/data/UserPost.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.data
import androidx.compose.runtime.Immutable
import kotlinx.serialization.Serializable
@Serializable
@Immutable
data class UserPost(
val id: String,
val userNickname: String,
val userFullName: String,
val userAvatar: String,
val text: String,
val images: List,
val likes: Int,
val replyCount: Int,
val messages: Int,
val comments: List,
val createdAt: Long,
val reply: UserPost?,
val poll: Poll?
)
================================================
FILE: app/src/main/java/dev/serhiiyaremych/imla/ui/theme/Color.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
================================================
FILE: app/src/main/java/dev/serhiiyaremych/imla/ui/theme/Theme.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Color.Cyan,
secondary = PurpleGrey80,
tertiary = Pink80,
surface = Color(0xFF101010L),
onSurface = Color.LightGray,
primaryContainer = Color.Magenta
)
private val LightColorScheme = lightColorScheme(
primary = Color.Blue,
secondary = Purple40,
tertiary = Pink40,
surface = Color.White,
onSurface = Color.Black,
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun ImlaTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
================================================
FILE: app/src/main/java/dev/serhiiyaremych/imla/ui/theme/Type.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
================================================
FILE: app/src/main/java/dev/serhiiyaremych/imla/ui/userpost/PollView.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.ui.userpost
import androidx.annotation.FloatRange
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.serhiiyaremych.imla.data.Poll
import dev.serhiiyaremych.imla.data.PollPosition
import dev.serhiiyaremych.imla.ui.theme.ImlaTheme
import kotlin.math.roundToInt
@Composable
fun PollView(poll: Poll, compactMode: Boolean, modifier: Modifier = Modifier) {
val borderColor = MaterialTheme.colorScheme.primary
val buttonBorder = remember(borderColor) {
BorderStroke(1.dp, borderColor)
}
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
for (pollPosition in poll.positions) {
key(pollPosition) {
if (poll.isCompleted) {
val progress = pollPosition.voted / poll.totalVotes.toFloat()
PollProgressLine(progress = progress, pollPosition.text, compactMode)
} else {
PollButton(buttonBorder, pollPosition.text, compactMode)
}
}
}
Text(
text = "${poll.totalVotes} votes",
color = MaterialTheme.colorScheme.onSurface,
fontSize = 12.sp
)
}
}
@Composable
private fun PollProgressLine(
@FloatRange(from = 0.0, to = 1.0) progress: Float,
label: String,
compactMode: Boolean
) {
var pollProgress by rememberSaveable {
mutableFloatStateOf(0.0f)
}
val animatedProgress by animateFloatAsState(
targetValue = pollProgress,
animationSpec = tween(1500),
label = "animatedProgress"
)
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = ButtonDefaults.MinHeight + 4.dp)
.drawBehind {
val cornerSize = 6.dp.toPx()
val width = (size.width * animatedProgress).coerceAtLeast(cornerSize)
drawRoundRect(
color = Color.LightGray,
size = Size(
width = width,
height = size.height
),
cornerRadius = CornerRadius(cornerSize, cornerSize)
)
},
contentAlignment = Alignment.CenterStart
) {
Row(modifier = Modifier.fillMaxWidth()) {
val textSize = if (compactMode) 12.sp else 14.sp
val text = remember(progress.roundToInt()) {
"${(progress * 100).roundToInt().coerceIn(0, 100)}%"
}
Text(
modifier = Modifier
.weight(1.0f)
.padding(horizontal = 8.dp),
text = label,
color = MaterialTheme.colorScheme.onBackground,
fontSize = textSize
)
Text(
text = text,
color = MaterialTheme.colorScheme.onBackground,
fontSize = textSize
)
}
}
SideEffect {
pollProgress = progress
}
}
@Composable
private fun PollButton(
buttonBorder: BorderStroke,
title: String,
compactMode: Boolean
) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
border = buttonBorder,
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
onClick = { /*TODO*/ }
) {
val textSize = if (compactMode) 12.sp else 14.sp
Text(
text = title,
color = MaterialTheme.colorScheme.primary,
fontSize = textSize
)
}
}
@Preview(name = "Poll not completed", device = Devices.PIXEL)
@Composable
private fun PollNotCompletedPreview() {
ImlaTheme {
val poll = Poll(
isCompleted = false,
totalVotes = 0,
positions = listOf(
PollPosition("Option 1", 0),
PollPosition("Option 2", 0),
PollPosition("Option 3", 0),
)
)
PollView(modifier = Modifier.fillMaxWidth(), poll = poll, compactMode = false)
}
}
@Preview(name = "Poll completed", device = Devices.PIXEL)
@Composable
private fun PollCompletedPreview() {
ImlaTheme {
val poll = Poll(
isCompleted = true,
totalVotes = 100,
positions = listOf(
PollPosition("Option 0", 12),
PollPosition("Option 1", 18),
PollPosition("Option 2", 60),
PollPosition("Option 3\ndddd\nddddf", 20),
)
)
PollView(modifier = Modifier.fillMaxWidth(), poll = poll, compactMode = false)
}
}
================================================
FILE: app/src/main/java/dev/serhiiyaremych/imla/ui/userpost/PostImageView.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.ui.userpost
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import dev.serhiiyaremych.imla.ui.theme.ImlaTheme
const val LOW_IMAGE_QUALITY = 0.2f
const val HIGH_IMAGE_QUALITY = 0.5f
private val imageCorners = RoundedCornerShape(4.dp)
private val startCorners = RoundedCornerShape(topStart = 4.dp, bottomStart = 4.dp)
private val endCorners = RoundedCornerShape(topEnd = 4.dp, bottomEnd = 4.dp)
private val topStartCorner = RoundedCornerShape(topStart = 4.dp)
private val topEndCorner = RoundedCornerShape(topEnd = 4.dp)
private val bottomStartCorner = RoundedCornerShape(bottomStart = 4.dp)
private val bottomEndCorner = RoundedCornerShape(bottomEnd = 4.dp)
/*
Mono image layout:
--------
| |
| A |
| |
--------
*/
@Composable
fun MonoImage(
imageUrl: String,
imageHeight: Dp,
compactMode: Boolean,
onImageClick: (String) -> Unit
) {
SinglePostImage(
modifier = Modifier
.height(imageHeight)
.fillMaxWidth()
.clickable { onImageClick(imageUrl) },
clipShape = imageCorners,
imgUrl = imageUrl,
compactMode = compactMode
)
}
/*
Duo image layout:
--------
| | |
| A | B |
| | |
--------
*/
@Composable
fun DuoImage(
imgA: String,
imgB: String,
imageHeight: Dp,
compactMode: Boolean,
onImageClick: (String) -> Unit
) {
Row(modifier = Modifier.fillMaxWidth()) {
// Image A
SinglePostImage(
modifier = Modifier
.height(imageHeight)
.weight(1.0f)
.clickable { onImageClick(imgA) },
clipShape = startCorners,
imgUrl = imgA,
compactMode = compactMode
)
Spacer(modifier = Modifier.width(4.dp))
// Image B
SinglePostImage(
modifier = Modifier
.height(imageHeight)
.weight(1.0f)
.clickable { onImageClick(imgB) },
clipShape = endCorners,
imgUrl = imgB,
compactMode = compactMode
)
}
}
/*
Triple image layout:
--------
| | B |
| A |-- |
| | C |
--------
*/
@Composable
fun TripleImage(
imageHeight: Dp,
imgA: String,
imgB: String,
imgC: String,
compactMode: Boolean,
onImageClick: (String) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
) {
// Image A
SinglePostImage(
modifier = Modifier
.height(imageHeight)
.weight(1.0f)
.clickable { onImageClick(imgA) },
clipShape = startCorners,
imgUrl = imgA,
compactMode = compactMode
)
Spacer(modifier = Modifier.width(4.dp))
Column(
modifier = Modifier
.fillMaxHeight()
.weight(1.0f)
) {
var imgBCSize by remember { mutableStateOf(IntSize.Zero) }
// Image B
SinglePostImage(
modifier = Modifier
.height((imageHeight / 2) - 2.dp)
.fillMaxWidth()
.onPlaced { imgBCSize = it.size }
.clickable { onImageClick(imgB) },
clipShape = topEndCorner,
imgUrl = imgB,
compactMode = compactMode
)
Spacer(modifier = Modifier.height(4.dp))
// Image C
SinglePostImage(
modifier = Modifier
.height((imageHeight / 2) - 2.dp)
.fillMaxWidth()
.clickable { onImageClick(imgC) },
clipShape = bottomEndCorner,
imgUrl = imgC,
compactMode = compactMode
)
}
}
}
/*
Quad image layout:
--------
| A | B |
| --|-- |
| C | D |
--------
*/
@Composable
fun QuadImage(
imageHeight: Dp,
imgA: String,
imgB: String,
imgC: String,
imgD: String,
compactMode: Boolean,
onImageClick: (String) -> Unit
) {
// static 2x2 grid
val cols = 2
Layout(
modifier = Modifier
.height(imageHeight)
.fillMaxWidth(),
content = {
SinglePostImage(
modifier = Modifier
.padding(end = 2.dp, bottom = 2.dp)
.clickable { onImageClick(imgA) },
clipShape = topStartCorner,
imgUrl = imgA,
compactMode = compactMode
)
SinglePostImage(
modifier = Modifier
.padding(start = 2.dp, bottom = 2.dp)
.clickable { onImageClick(imgB) },
clipShape = topEndCorner,
imgUrl = imgB,
compactMode = compactMode
)
SinglePostImage(
modifier = Modifier
.padding(top = 2.dp, end = 2.dp)
.clickable { onImageClick(imgC) },
clipShape = bottomStartCorner,
imgUrl = imgC,
compactMode = compactMode
)
SinglePostImage(
modifier = Modifier
.padding(top = 2.dp, start = 2.dp)
.clickable { onImageClick(imgD) },
clipShape = bottomEndCorner,
imgUrl = imgD,
compactMode = compactMode
)
}
) { measurables, constraints ->
val rows = (measurables.size / cols) + measurables.size % cols
val childWidth = constraints.maxWidth / cols
val childHeight = constraints.maxHeight / rows
val children = measurables.map { child ->
child.measure(
constraints.copy(
minWidth = childWidth,
minHeight = childHeight,
maxWidth = childWidth,
maxHeight = childHeight
)
)
}
layout(constraints.maxWidth, constraints.maxHeight) {
children.forEachIndexed { idx, child ->
val row = idx / cols
val col = idx - (row * cols)
val offset = IntOffset(
x = col * child.measuredWidth,
y = row * child.measuredHeight
)
child.placeRelative(offset)
}
}
}
}
@Composable
fun SinglePostImage(
modifier: Modifier,
clipShape: Shape,
imgUrl: String,
compactMode: Boolean
) {
val imageLayoutSize = remember {
mutableStateOf(IntSize.Zero)
}
val context = LocalContext.current
val imageSize = imageLayoutSize.value
val request = remember(imageSize, imgUrl, context, compactMode) {
ImageRequest.Builder(context)
.data(imgUrl)
.setImageSize(
width = imageSize.width,
height = imageSize.height,
compactMode = compactMode
)
.crossfade(false)
.build()
}
AsyncImage(
modifier = modifier
.clip(clipShape)
.onPlaced { imageLayoutSize.value = it.size }
.let { if (LocalInspectionMode.current) it.background(Color.Magenta) else it },
model = request,
contentScale = ContentScale.Crop,
contentDescription = null
)
}
/*
Image row layout:
-------------------
| A | B | C | ... |
-------------------
*/
@Composable
fun ImageRow(
imageHeight: Dp,
images: List,
compactMode: Boolean,
onImageClick: (String) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(state = rememberScrollState())
) {
images.forEachIndexed { idx, image ->
key(image) {
val clipper = when (idx) {
0 -> startCorners
images.lastIndex -> endCorners
else -> RectangleShape
}
SinglePostImage(
modifier = Modifier
.size(imageHeight)
.clickable { onImageClick(image) },
clipShape = clipper,
imgUrl = image,
compactMode = compactMode
)
if (idx < images.lastIndex) {
Spacer(modifier = Modifier.width(4.dp))
}
}
}
}
}
private fun ImageRequest.Builder.setImageSize(
width: Int,
height: Int,
compactMode: Boolean
): ImageRequest.Builder {
val quality = if (compactMode) LOW_IMAGE_QUALITY else HIGH_IMAGE_QUALITY
return if (width > 0 && height > 0) size(
width = (width * quality).toInt(),
height = (height * quality).toInt()
) else this
}
@Preview
@Composable
private fun MonoPreview() {
ImlaTheme {
MonoImage(imageUrl = "", imageHeight = 150.dp, compactMode = false) {}
}
}
@Preview
@Composable
private fun DuoPreview() {
ImlaTheme {
DuoImage(imgA = "", imgB = "", imageHeight = 150.dp, compactMode = false) {}
}
}
@Preview
@Composable
private fun TriplePreview() {
ImlaTheme {
TripleImage(imgA = "", imgB = "", imgC = "", imageHeight = 150.dp, compactMode = false) {}
}
}
================================================
FILE: app/src/main/java/dev/serhiiyaremych/imla/ui/userpost/SimpleImageViewer.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.ui.userpost
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil.compose.AsyncImage
import coil.request.ImageRequest
import coil.size.Size
@Composable
fun SimpleImageViewer(
modifier: Modifier,
imageUrl: String,
onDismiss: () -> Unit
) {
BackHandler(true) {
onDismiss()
}
val context = LocalContext.current
val request = remember(imageUrl, context) {
ImageRequest.Builder(context)
.data(imageUrl)
.size(Size.ORIGINAL)
.crossfade(true)
.build()
}
AsyncImage(
modifier = modifier.clickable(onClick = onDismiss),
model = request,
contentDescription = null,
contentScale = ContentScale.Fit
)
}
================================================
FILE: app/src/main/java/dev/serhiiyaremych/imla/ui/userpost/UserPostView.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.ui.userpost
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.Message
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.serhiiyaremych.imla.data.UserPost
import java.time.Instant
import java.util.concurrent.TimeUnit
private const val SHOW_ICONS = true
private val AvatarSizeLarge = 44.dp
private val AvatarSizeSmall = 22.dp
@Composable
fun UserPostView(
post: UserPost,
modifier: Modifier = Modifier,
compactMode: Boolean = false,
monochromeBottomIcons: Boolean = false,
onImageClick: (imageUrl: String) -> Unit = {},
onItemClick: (id: String) -> Unit = {}
) {
val contentPadding = if (compactMode) 8.dp else 16.dp
Box(
modifier = modifier
.fillMaxWidth()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { onItemClick.invoke(post.id) }
)
.padding(contentPadding)
) {
UserLine(post, compactMode)
Column(
modifier = Modifier
.padding(start = if (compactMode) 0.dp else contentPadding)
.padding(
start = if (compactMode) 0.dp else AvatarSizeLarge,
top = if (compactMode) AvatarSizeSmall else AvatarSizeLarge / 2
)
.fillMaxWidth()
) {
Spacer(modifier = Modifier.height(8.dp))
val textSize = if (compactMode) 12.sp else 14.sp
Text(
text = post.text,
style = MaterialTheme.typography.bodyLarge.copy(fontSize = textSize)
)
if (post.images.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
ImageList(
images = post.images,
compactMode = compactMode,
onImageClick = onImageClick
)
}
if (post.reply != null) {
Spacer(modifier = Modifier.height(8.dp))
ReplyView(post.reply)
}
if (post.poll != null) {
Spacer(modifier = Modifier.height(8.dp))
PollView(poll = post.poll, compactMode = compactMode)
}
if (compactMode.not()) {
TweetFooter(post, monochromeBottomIcons)
}
}
}
}
@Composable
private fun ReplyView(tweet: UserPost) {
UserPostView(
modifier = Modifier
.height(IntrinsicSize.Min)
.border(1.dp, Color.LightGray, RoundedCornerShape(6.dp))
.padding(2.dp),
post = tweet,
compactMode = true
)
}
@Composable
private fun UserLine(tweet: UserPost, compactMode: Boolean) {
val nameColor = MaterialTheme.colorScheme.onBackground
val userString =
remember(tweet.userFullName, tweet.userNickname, tweet.createdAt, compactMode, nameColor) {
buildUserString(
fullName = tweet.userFullName,
nickName = tweet.userNickname,
createdAt = tweet.createdAt,
compactMode = compactMode,
nameColor = nameColor
)
}
Row {
Avatar(userAvatar = tweet.userAvatar, compactMode = compactMode)
Spacer(modifier = Modifier.width(if (compactMode) 8.dp else 16.dp))
Text(text = userString, style = MaterialTheme.typography.bodySmall)
}
}
@Composable
private fun TweetFooter(tweet: UserPost, monochromeBottomIcons: Boolean) {
Spacer(modifier = Modifier.height(8.dp))
Row(
horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier.fillMaxWidth()
) {
IconWithText(
icon = Icons.Outlined.Message,
text = "${tweet.messages}",
iconTint = if (monochromeBottomIcons) Color.LightGray else MaterialTheme.colorScheme.primary
)
IconWithText(
icon = Icons.Outlined.Refresh,
text = "${tweet.replyCount}",
iconTint = if (monochromeBottomIcons) Color.LightGray else MaterialTheme.colorScheme.secondary
)
IconWithText(
icon = Icons.Outlined.Favorite,
text = "${tweet.likes}",
iconTint = if (monochromeBottomIcons) Color.LightGray else MaterialTheme.colorScheme.tertiary
)
IconWithText(
icon = Icons.Outlined.Share,
text = "",
iconTint = if (monochromeBottomIcons) Color.LightGray else Color.LightGray
)
}
}
private fun buildUserString(
fullName: String,
nickName: String,
createdAt: Long,
compactMode: Boolean,
nameColor: Color
) = buildAnnotatedString {
withStyle(
SpanStyle(
fontSize = 13.sp,
fontWeight = FontWeight.Black,
color = nameColor
)
) {
append(fullName)
}
append(" ")
withStyle(
SpanStyle(
fontSize = 12.sp,
fontWeight = FontWeight.Normal,
color = Color.DarkGray
)
) {
append(nickName)
append(" • ")
append(formattedTime(createdAt, compactMode))
}
}
private fun formattedTime(createdAt: Long, compactMode: Boolean): String {
val hours = if (compactMode) "h" else " hours"
val minutes = if (compactMode) "min" else " minutes"
val timeAgo = buildString {
val now = Instant.now()
val diff = now.minusMillis(createdAt).toEpochMilli()
val hrs = TimeUnit.MILLISECONDS.toHours(diff)
val minutesTime = if (hrs > 0) 0L else TimeUnit.MILLISECONDS.toMinutes(diff)
if (hrs > 0) {
when (hrs) {
1L -> if (compactMode) append("1h ago") else append("an hour ago")
else -> append("$hrs$hours ago")
}
append(" ")
}
if (minutesTime > 0) {
when (minutesTime) {
1L -> append("now")
else -> append("$minutesTime$minutes ago")
}
}
if (this.isBlank()) {
append("now")
}
}
return timeAgo
}
@Composable
fun Avatar(userAvatar: String, compactMode: Boolean) {
val imgLayoutSizeDp = if (compactMode) AvatarSizeSmall else AvatarSizeLarge
SinglePostImage(
modifier = Modifier.size(imgLayoutSizeDp),
clipShape = CircleShape,
imgUrl = userAvatar,
compactMode = compactMode
)
}
@Composable
fun ImageList(
images: List,
compactMode: Boolean,
imageRowHeight: Dp = if (compactMode) 125.dp else 150.dp,
onImageClick: (imageUrl: String) -> Unit
) {
when (images.size) {
1 -> MonoImage(images[0], imageRowHeight, compactMode, onImageClick)
2 -> DuoImage(images[0], images[1], imageRowHeight, compactMode, onImageClick)
3 -> TripleImage(
imageHeight = imageRowHeight,
imgA = images[0],
imgB = images[1],
imgC = images[2],
compactMode = compactMode,
onImageClick = onImageClick
)
4 -> QuadImage(
imageHeight = imageRowHeight + 56.dp,
imgA = images[0],
imgB = images[1],
imgC = images[2],
imgD = images[3],
compactMode = compactMode,
onImageClick = onImageClick
)
else -> ImageRow(imageRowHeight, images, compactMode, onImageClick)
}
}
@Composable
private fun IconWithText(icon: ImageVector, text: String, iconTint: Color) {
CompositionLocalProvider(
(LocalContentColor provides iconTint)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (SHOW_ICONS) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
}
if (text.isNotEmpty()) {
Spacer(modifier = Modifier.width(6.dp))
Text(
text = text,
style = MaterialTheme.typography.labelMedium.copy(fontSize = 14.sp)
)
}
}
}
}
================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
================================================
FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: app/src/main/res/values/colors.xml
================================================
================================================
FILE: app/src/main/res/values/strings.xml
================================================
Imla
================================================
FILE: app/src/main/res/values/themes.xml
================================================
================================================
FILE: app/src/main/res/xml/backup_rules.xml
================================================
================================================
FILE: app/src/main/res/xml/data_extraction_rules.xml
================================================
================================================
FILE: app/src/test/java/dev/serhiiyaremych/imla/ExampleUnitTest.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
================================================
FILE: benchmark/.gitignore
================================================
/build
================================================
FILE: benchmark/build.gradle.kts
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
plugins {
alias(libs.plugins.android.test)
alias(libs.plugins.jetbrains.kotlin.android)
}
android {
namespace = "dev.serhiiyaremych.benchmark"
compileSdk = 34
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
defaultConfig {
minSdk = 26
targetSdk = 34
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
// This benchmark buildType is used for benchmarking, and should function like your
// release build (for example, with minification on). It"s signed with a debug key
// for easy local/CI testing.
create("benchmark") {
isDebuggable = true
signingConfig = getByName("debug").signingConfig
matchingFallbacks += listOf("release")
}
}
targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true
}
dependencies {
implementation(libs.androidx.junit)
implementation(libs.androidx.espresso.core)
implementation(libs.androidx.uiautomator)
implementation(libs.androidx.benchmark.macro.junit4)
}
androidComponents {
beforeVariants(selector().all()) {
it.enable = it.buildType == "benchmark"
}
}
================================================
FILE: benchmark/src/main/AndroidManifest.xml
================================================
================================================
FILE: benchmark/src/main/java/dev/serhiiyaremych/benchmark/ImlaBenchmark.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.benchmark
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.ExperimentalMetricApi
import androidx.benchmark.macro.FrameTimingGfxInfoMetric
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.TraceSectionMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* This is an example startup benchmark.
*
* It navigates to the device's home screen, and launches the default activity.
*
* Before running this benchmark:
* 1) switch your app's active build variant in the Studio (affects Studio runs only)
* 2) add `` to your app's manifest, within the `` tag
*
* Run this benchmark from Studio to see startup measurements, and captured system traces
* for investigating your app's performance.
*/
@RunWith(AndroidJUnit4::class)
class ImlaBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@OptIn(ExperimentalMetricApi::class)
@Test
fun measureRendering() = benchmarkRule.measureRepeated(
packageName = "dev.serhiiyaremych.imla",
compilationMode = CompilationMode.Full(),
metrics = listOf(
FrameTimingMetric(),
FrameTimingGfxInfoMetric(),
metricSection("RenderingPipeline#applyAllEffects", TraceSectionMetric.Mode.Average),
metricSection("RenderObject#onRender", TraceSectionMetric.Mode.Average),
metricSection("copyExtTextureToFrameBuffer", TraceSectionMetric.Mode.Average),
metricSection("fullSizeBuffer", TraceSectionMetric.Mode.Average),
),
iterations = 4,
startupMode = StartupMode.WARM
) {
pressHome()
startActivityAndWait()
val uiDevice = this.device
val width = uiDevice.displayWidth
val height = uiDevice.displayHeight
repeat(3) {
uiDevice.awaitComposeIdle()
uiDevice.swipe(
width / 2,
(height * 0.75).toInt(),
width / 2,
(height * 0.25).toInt(),
8
)
}
}
@OptIn(ExperimentalMetricApi::class)
private fun metricSection(label: String, mode: TraceSectionMetric.Mode) = TraceSectionMetric(
sectionName = label,
mode = mode
)
private fun UiDevice.awaitComposeIdle(timeout: Long = 3000) {
wait(Until.findObject(By.desc("COMPOSE-IDLE")), timeout)
}
}
================================================
FILE: build.gradle.kts
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.jetbrains.kotlin.serialization) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.android.test) apply false
}
================================================
FILE: gradle/libs.versions.toml
================================================
[versions]
agp = "8.9.1"
coilCompose = "2.7.0"
collection = "1.5.0"
composeBomAlpha = "2025.03.00"
desugar_jdk_libs = "2.1.5"
graphicsCore = "1.0.2"
haze = "1.5.2"
kotlin = "2.1.20"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
kotlinxCollectionsImmutable = "0.3.8"
kotlinxSerializationJson = "1.8.0"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.1"
#composeBom = "2024.04.01"
appcompat = "1.7.0"
material = "1.12.0"
kotlinMath = "1.6.0"
runtimeTracing = "1.7.8"
tracingKtx = "1.3.0-beta01"
uiautomator = "2.3.0"
benchmarkMacroJunit4 = "1.3.3"
[libraries]
androidx-collection = { module = "androidx.collection:collection", version.ref = "collection" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-foundation = { module = "androidx.compose.foundation:foundation" }
androidx-graphics-core = { module = "androidx.graphics:graphics-core", version.ref = "graphicsCore" }
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
androidx-runtime-tracing = { module = "androidx.compose.runtime:runtime-tracing", version.ref = "runtimeTracing" }
androidx-tracing-ktx = { module = "androidx.tracing:tracing-ktx", version.ref = "tracingKtx" }
androidx-ui-util = { module = "androidx.compose.ui:ui-util" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
compose-bom = { module = "androidx.compose:compose-bom-alpha", version.ref = "composeBomAlpha" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
#androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
#material3 = { module = "androidx.compose.material3:material3" }
kotlin-math = { module = "dev.romainguy:kotlin-math", version.ref = "kotlinMath" }
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
android-test = { id = "com.android.test", version.ref = "agp" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#
# /*
# * Copyright 2025, Serhii Yaremych
# * SPDX-License-Identifier: MIT
# */
#
#Fri Sep 27 19:22:38 EEST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
================================================
FILE: gradle.properties
================================================
#
# Copyright 2024, Serhii Yaremych
# SPDX-License-Identifier: MIT
#
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
#
# Copyright 2024, Serhii Yaremych
# SPDX-License-Identifier: MIT
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: imla/.gitignore
================================================
/build
================================================
FILE: imla/build.gradle.kts
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.compose.compiler)
}
android {
namespace = "dev.serhiiyaremych.imla"
compileSdk = 34
defaultConfig {
minSdk = 23
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
create("benchmark") {
initWith(buildTypes.getByName("release"))
matchingFallbacks += listOf("release")
}
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
explicitApi()
}
kotlinOptions {
freeCompilerArgs += "-Xcontext-receivers"
jvmTarget = "17"
}
buildFeatures {
compose = true
shaders = true
dataBinding = false
viewBinding = false
mlModelBinding = false
aidl = false
buildConfig = true
}
lint {
abortOnError = true
warningsAsErrors = true
}
}
dependencies {
api(platform(libs.compose.bom))
implementation(libs.androidx.ui.util)
implementation(libs.androidx.collection)
implementation(libs.kotlin.math)
implementation(libs.androidx.foundation)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.graphics.core)
implementation(libs.androidx.runtime.tracing)
implementation(libs.androidx.tracing.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
================================================
FILE: imla/consumer-rules.pro
================================================
================================================
FILE: imla/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontobfuscate
================================================
FILE: imla/src/androidTest/java/dev/serhiiyaremych/imla/ExampleInstrumentedTest.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("dev.serhiiyaremych.imla.test", appContext.packageName)
}
}
================================================
FILE: imla/src/main/AndroidManifest.xml
================================================
================================================
FILE: imla/src/main/assets/shader/blur_down.frag
================================================
#version 300 es
precision mediump float;
#define GAMMA 2.2
uniform sampler2D u_Texture;
uniform vec2 u_Texel;
uniform vec2 u_ContentOffset;
in vec2 texCoord;
out vec4 color;
const vec2 S = vec2(-1, 1);
const float WEIGHT_SUM_INV = 1.0 / 3.125;
const vec2 UV_MIN = vec2(0.0);
const vec2 UV_MAX = vec2(1.0);
// Simple approximation
// Convert sRGB color to linear space
vec4 gammaDecode(vec4 rgba) {
vec3 rgb = rgba.rgb;
return vec4(rgb * rgb, rgba.a);
}
// Convert linear color to sRGB space
vec4 gammaEncode(vec4 rgba) {
vec3 rgb = rgba.rgb;
return vec4(sqrt(rgb), rgba.a);
}
vec4 safeTexture(sampler2D tex, vec2 uv) {
// return texture(tex, clamp(uv, UV_MIN + u_ContentOffset, UV_MAX - u_ContentOffset));
return texture(tex, clamp(uv, UV_MIN, UV_MAX));
}
// Credits:
// Jorge Jimenez,
// NEXT GENERATION POST PROCESSING IN CALL OF DUTY: ADVANCED WARFARE, https://advances.realtimerendering.com/s2014/index.html
// . . . . . . .
// . A . B . C .
// . . D . E . .
// . F . G . H .
// . . I . J . .
// . K . L . M .
// . . . . . . .
// Temporally stable box filtering
void main() {
vec2 uv = texCoord;
// center point
vec4 sum = gammaDecode(safeTexture(u_Texture, uv)) * 0.125; // G
// inner ring corners
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * S)) * 0.5; // D
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * S.yy)) * 0.5; // E
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * S.yx)) * 0.5; // I
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * S.xx)) * 0.5; // J
// outer ring corners
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * S * 2.0)) * 0.125; // A
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * S.yy * 2.0)) * 0.125; // C
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * S.yx * 2.0)) * 0.125; // M
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * S.xx * 2.0)) * 0.125; // K
// middle ring sides
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * vec2(S.x, 0.0) * 2.0)) * 0.125; // F
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * vec2(0.0, S.y) * 2.0)) * 0.125; // B
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * vec2(S.y, 0.0) * 2.0)) * 0.125; // H
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * vec2(0.0, S.x) * 2.0)) * 0.125; // L
sum *= WEIGHT_SUM_INV;
color = gammaEncode(sum);
}
================================================
FILE: imla/src/main/assets/shader/blur_quad.frag
================================================
#version 300 es
precision mediump float;
#define GAMMA 2.2
struct VertexOutput
{
float texIndex;
float flipTexture;
float isExternalTexture;
float alpha;
float mask;
};
// horiz=(1.0, 0.0), vert=(0.0, 1.0)
uniform vec2 u_BlurDirection;
uniform vec2 u_TexelSize;
uniform float u_BlurSigma;
uniform vec4 u_BlurTint;
uniform sampler2D u_Textures[8];
in vec2 maskCoord;
in vec2 texCoord;
in VertexOutput data;
out vec4 color;
//Classic gamma correction functions
vec3 linear_from_srgb(vec3 srgb) {
return vec3(
srgb.r <= 0.04045 ? srgb.r / 12.92 : pow((srgb.r + 0.055) / 1.055, GAMMA),
srgb.g <= 0.04045 ? srgb.g / 12.92 : pow((srgb.g + 0.055) / 1.055, GAMMA),
srgb.b <= 0.04045 ? srgb.b / 12.92 : pow((srgb.b + 0.055) / 1.055, GAMMA)
);
}
vec3 srgb_from_linear(vec3 lin) {
return vec3(
lin.r <= 0.0031308 ? lin.r * 12.92 : 1.055 * pow(lin.r, 1.0 / GAMMA) - 0.055,
lin.g <= 0.0031308 ? lin.g * 12.92 : 1.055 * pow(lin.g, 1.0 / GAMMA) - 0.055,
lin.b <= 0.0031308 ? lin.b * 12.92 : 1.055 * pow(lin.b, 1.0 / GAMMA) - 0.055
);
}
float gaussianWeight(float x, float sigma) {
return exp(-0.5 * (x * x) / (sigma * sigma));
}
void main() {
bool flipTexture = int(data.flipTexture) > 0;
vec2 loc = mix(texCoord, vec2(texCoord.x, 1.0 - texCoord.y), data.flipTexture);
vec2 dir = u_BlurDirection / u_TexelSize;
vec4 acc = vec4(0.0);
float totalWeight = 0.0;
int support = int(u_BlurSigma) * 3;
float sigma = u_BlurSigma;
// int step = 9; // creates an interesting effect of embossed glass
int step = 1;
for (int i = -support; i <= support; i += step) {
float fi = float(i);
float x = fi;
float weight = gaussianWeight(x, sigma);
vec4 texColor = texture(u_Textures[1], loc + x * dir); // todo: support batching
texColor.rgb = linear_from_srgb(texColor.rgb);
acc += texColor * weight;
totalWeight += weight;
}
acc.rgb = srgb_from_linear(acc.rgb * (1.0 / totalWeight));
acc.a *= (1.0 / totalWeight);
color = mix(acc, u_BlurTint, u_BlurTint.a * u_BlurTint.a);
}
================================================
FILE: imla/src/main/assets/shader/blur_up.frag
================================================
#version 300 es
precision mediump float;
#define GAMMA 2.2
uniform sampler2D u_Texture;
uniform vec2 u_Texel;
uniform vec4 u_Tint;
in vec2 texCoord;
out vec4 color;
const vec2 S = vec2(-1, 1);
const float WEIGHT_SUM_INV = 1.0 / 16.0;
const vec2 UV_MIN = vec2(0.0);
const vec2 UV_MAX = vec2(1.0);
// Simple approximation
// Convert sRGB color to linear space
vec4 gammaDecode(vec4 rgba) {
vec3 rgb = rgba.rgb;
return vec4(rgb * rgb, rgba.a);
}
// Convert linear color to sRGB space
vec4 gammaEncode(vec4 rgba) {
vec3 rgb = rgba.rgb;
return vec4(sqrt(rgb), rgba.a);
}
vec4 safeTexture(sampler2D tex, vec2 uv) {
return texture(tex, clamp(uv, UV_MIN, UV_MAX));
}
// Credits:
// Jorge Jimenez,
// NEXT GENERATION POST PROCESSING IN CALL OF DUTY: ADVANCED WARFARE, https://advances.realtimerendering.com/s2014/index.html
// 9-tap bilinear upsampler (tent filter)
// [ 1 2 1 ]
// 1/16 [ 2 4 2 ]
// [ 1 2 1 ]
void main() {
vec2 uv = texCoord;
// center point
vec4 sum = gammaDecode(safeTexture(u_Texture, uv)) * 4.0;
// corners
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * S)); // tl
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * S.yy)); // tr
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * S.yx)); // br
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * S.xx)); // bl
// sides
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * vec2(S.x, 0.0))) * 2.0; // ml
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * vec2(0.0, S.y))) * 2.0; // mt
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * vec2(S.y, 0.0))) * 2.0; // mr
sum += gammaDecode(safeTexture(u_Texture, uv + u_Texel * vec2(0.0, S.x))) * 2.0; // mb
sum *= WEIGHT_SUM_INV;
color = gammaEncode(sum);
color = mix(color, u_Tint, u_Tint.a * u_Tint.a);
}
================================================
FILE: imla/src/main/assets/shader/default_quad.frag
================================================
#version 300 es
#extension GL_OES_standard_derivatives: enable
precision mediump float;
struct VertexOutput
{
float texIndex;
float flipTexture;
float isExternalTexture;
float alpha;
float mask;
};
uniform sampler2D u_Textures[8]; // todo: pre-process source before compilation to set HW value
in vec2 maskCoord;
in vec2 texCoord;
in VertexOutput data;
out vec4 color;
void main()
{
vec4 baseColor = vec4(1.);
vec2 texCoord = mix(texCoord, vec2(texCoord.x, 1.0 - texCoord.y), data.flipTexture);
switch (int(data.texIndex)) {
case 0:
baseColor = texture(u_Textures[0], texCoord);break;
case 1:
baseColor = texture(u_Textures[1], texCoord);break;
case 2:
baseColor = texture(u_Textures[2], texCoord);break;
case 3:
baseColor = texture(u_Textures[3], texCoord);break;
case 4:
baseColor = texture(u_Textures[4], texCoord);break;
case 5:
baseColor = texture(u_Textures[5], texCoord);break;
case 6:
baseColor = texture(u_Textures[6], texCoord);break;
case 7:
baseColor = texture(u_Textures[7], texCoord);break;
}
baseColor.a = data.alpha;
color = baseColor;
}
================================================
FILE: imla/src/main/assets/shader/default_quad.vert
================================================
#version 300 es
precision mediump float;
uniform mat4 u_ViewProjection;
layout (location = 0) in vec2 a_TexCoord;
layout (location = 1) in vec4 a_Position;
layout (location = 2) in float a_TexIndex;
layout (location = 3) in float a_FlipTexture;
layout (location = 4) in float a_IsExternalTexture;
layout (location = 5) in float a_Alpha;
layout (location = 6) in float a_Mask;
layout (location = 7) in vec2 a_MaskCoord;
struct VertexOutput
{
float texIndex;
float flipTexture;
float isExternalTexture;
float alpha;
float mask;
};
out vec2 maskCoord;
out vec2 texCoord;
out VertexOutput data;
void main() {
maskCoord = a_MaskCoord;
texCoord = a_TexCoord;
data.texIndex = a_TexIndex;
data.flipTexture = a_FlipTexture;
data.isExternalTexture = a_IsExternalTexture;
data.alpha = a_Alpha;
data.mask = a_Mask;
gl_Position = u_ViewProjection * a_Position;
}
================================================
FILE: imla/src/main/assets/shader/external_quad.frag
================================================
#version 300 es
#extension GL_OES_EGL_image_external_essl3: enable
#extension GL_OES_EGL_image_external: require
precision mediump float;
struct VertexOutput
{
float texIndex;
float flipTexture;
float isExternalTexture;
float alpha;
float mask;
};
uniform samplerExternalOES u_Textures[8];
in vec2 maskCoord;
in vec2 texCoord;
in VertexOutput data;
out vec4 color;
void main()
{
vec4 baseColor = vec4(1.);
vec2 texCoord = mix(texCoord, vec2(texCoord.x, 1.0 - texCoord.y), data.flipTexture);
switch (int(data.texIndex)) {
case 0:
baseColor = texture(u_Textures[0], texCoord);break;
case 1:
baseColor = texture(u_Textures[1], texCoord);break;
case 2:
baseColor = texture(u_Textures[2], texCoord);break;
case 3:
baseColor = texture(u_Textures[3], texCoord);break;
case 4:
baseColor = texture(u_Textures[4], texCoord);break;
case 5:
baseColor = texture(u_Textures[5], texCoord);break;
case 6:
baseColor = texture(u_Textures[6], texCoord);break;
case 7:
baseColor = texture(u_Textures[7], texCoord);break;
}
baseColor.a = data.alpha;
color = baseColor;
}
================================================
FILE: imla/src/main/assets/shader/mask.frag
================================================
#version 300 es
precision mediump float;
uniform sampler2D u_Background;
uniform sampler2D u_Mask;
uniform sampler2D u_Textures[8];
struct VertexOutput
{
float texIndex;
float flipTexture;
float isExternalTexture;
float alpha;
float mask;
};
in vec2 maskCoord;
in vec2 texCoord;
in VertexOutput data;
out vec4 color;
void main() {
vec4 maskColor = texture(u_Mask, maskCoord);
vec2 texCoord = mix(texCoord, vec2(texCoord.x, 1.0 - texCoord.y), data.flipTexture);
vec4 backgroundColor = texture(u_Background, vec2(maskCoord.x, 1. - maskCoord.y));
vec4 contentColor = vec4(1.0, 1.0, 1.0, 1.0);
switch (int(data.texIndex)) {
case 0:
contentColor = texture(u_Textures[0], texCoord);break;
case 1:
contentColor = texture(u_Textures[1], texCoord);break;
case 2:
contentColor = texture(u_Textures[2], texCoord);break;
case 3:
contentColor = texture(u_Textures[3], texCoord);break;
case 4:
contentColor = texture(u_Textures[4], texCoord);break;
case 5:
contentColor = texture(u_Textures[5], texCoord);break;
case 6:
contentColor = texture(u_Textures[6], texCoord);break;
case 7:
contentColor = texture(u_Textures[7], texCoord);break;
}
vec4 finalColor = contentColor;
finalColor.rgb = mix(backgroundColor.rgb, contentColor.rgb, maskColor.r);
color = finalColor;
}
================================================
FILE: imla/src/main/assets/shader/noise.frag
================================================
#version 300 es
#extension GL_OES_standard_derivatives: enable
precision mediump float;
#define GAMMA 2.2
in vec2 texCoord;
out vec4 color;
highp float rand(vec2 co)
{
highp float a = 12.9898;
highp float b = 78.233;
highp float c = 43758.5453;
highp float dt = dot(co.xy, vec2(a, b));
highp float sn = mod(dt, 3.14);
return fract(sin(sn) * c);
}
void main()
{
// color = vec4(1.0);
float val = pow(clamp(rand(texCoord), 0.5, 0.8), 1.0 / GAMMA);
color = vec4(vec3(val, val, val), 1.0);
}
================================================
FILE: imla/src/main/assets/shader/simple_blur.frag
================================================
#version 300 es
precision mediump float;
#define GAMMA 2.2
// horiz=(1.0, 0.0), vert=(0.0, 1.0)
uniform vec2 u_BlurDirection;
uniform float u_BlurSigma;
uniform vec4 u_BlurTint;
uniform vec2 u_TexelSize;
uniform sampler2D u_Texture;
in vec2 maskCoord;
in vec2 texCoord;
in float alpha;
out vec4 color;
//Classic gamma correction functions
vec3 linear_from_srgb(vec3 srgb) {
return srgb * srgb;
}
vec3 srgb_from_linear(vec3 lin) {
return sqrt(lin);
}
float gaussianWeight(float x, float sigma) {
return exp(-0.5 * (x * x) / (sigma * sigma));
}
void main() {
vec2 loc = texCoord;
vec2 dir = u_BlurDirection / u_TexelSize;
vec4 acc = vec4(0.0);
float totalWeight = 0.0;
int support = int(u_BlurSigma) * 3;
float sigma = u_BlurSigma;
// int step = 9; // creates an interesting effect of embossed glass
int step = 1;
for (int i = -support; i <= support; i += step) {
float fi = float(i);
float x = fi;
float weight = gaussianWeight(x, sigma);
vec4 texColor = texture(u_Texture, loc + x * dir);
texColor.rgb = linear_from_srgb(texColor.rgb);
acc += texColor * weight;
totalWeight += weight;
}
acc.rgb = srgb_from_linear(acc.rgb * (1.0 / totalWeight));
acc.a *= (1.0 / totalWeight);
color = mix(acc, u_BlurTint, u_BlurTint.a * u_BlurTint.a);
}
================================================
FILE: imla/src/main/assets/shader/simple_ext_quad.frag
================================================
#version 300 es
#extension GL_OES_EGL_image_external_essl3: enable
#extension GL_OES_EGL_image_external: require
precision mediump float;
uniform samplerExternalOES u_Texture;
in vec2 maskCoord;
in vec2 texCoord;
in vec2 texSize;
in float alpha;
in float flip;
out vec4 color;
void main()
{
vec4 baseColor = texture(u_Texture, texCoord);
baseColor.a = alpha;
color = baseColor;
}
================================================
FILE: imla/src/main/assets/shader/simple_mask.frag
================================================
#version 300 es
precision mediump float;
uniform sampler2D u_Background;
uniform sampler2D u_Mask;
uniform sampler2D u_Texture;
in vec2 maskCoord;
in vec2 texCoord;
out vec4 color;
void main() {
vec4 maskColor = texture(u_Mask, maskCoord);
vec4 backgroundColor = texture(u_Background, maskCoord);
vec4 contentColor = texture(u_Texture, vec2(texCoord.x, texCoord.y));
vec4 finalColor = contentColor;
finalColor.rgb = mix(backgroundColor.rgb, contentColor.rgb, maskColor.r);
color = finalColor;
}
================================================
FILE: imla/src/main/assets/shader/simple_quad.frag
================================================
#version 300 es
#extension GL_OES_standard_derivatives: enable
precision mediump float;
uniform sampler2D u_Texture;
in vec2 maskCoord;
in vec2 texCoord;
in float alpha;
out vec4 color;
void main()
{
vec4 baseColor = texture(u_Texture, texCoord);
baseColor.a *= alpha;
color = baseColor;
}
================================================
FILE: imla/src/main/assets/shader/simple_quad.vert
================================================
#version 300 es
precision mediump float;
layout (std140) uniform TextureDataUBO {
vec2 uv[4]; // x, y,
// vec2 size; // width, height
float flipTexture; // flip Y texture coordinate
float alpha; // alpha blending of the texture
} textureData;
layout (location = 0) in vec2 aPosition;
out vec2 maskCoord;
out vec2 texCoord;
//out vec2 texSize;
out float alpha;
void main() {
// Set the final position of the vertex in clip space
gl_Position = vec4(aPosition, 0.0, 1.0);
alpha = textureData.alpha;
maskCoord = aPosition * 0.5 + 0.5;
maskCoord.y = abs(textureData.flipTexture - maskCoord.y);
texCoord = textureData.uv[gl_VertexID % 4];
texCoord.y = abs(textureData.flipTexture - texCoord.y);
// texSize = textureData.size;
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/ext/util.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.ext
import android.opengl.GLES30
import android.util.Log
import androidx.tracing.trace
import dev.serhiiyaremych.imla.BuildConfig
import javax.microedition.khronos.egl.EGL10
import javax.microedition.khronos.egl.EGLContext
internal fun logw(tag: String, message: String) {
if (BuildConfig.DEBUG) Log.w(tag, message)
}
internal fun logd(tag: String, message: String) {
if (BuildConfig.DEBUG) Log.d(tag, message)
}
internal fun isGLThread(): Boolean {
val egl = EGLContext.getEGL() as? EGL10
return egl != null && egl.eglGetCurrentContext() != EGL10.EGL_NO_CONTEXT
}
@Suppress("UNUSED_PARAMETER", "NOTHING_TO_INLINE")
internal fun checkGlError(action: Unit = Unit) {
if (BuildConfig.DEBUG) {
trace("checkGlError") {
val error = GLES30.glGetError()
if (error != GLES30.GL_NO_ERROR) {
Log.e(
"checkGlError",
"failed with error $error on thread ${Thread.currentThread().name}"
)
throwError(error)
}
}
}
}
internal fun throwError(errorCode: Int) {
error(errorCode)
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/modifier/ImlaBlurSourceModifier.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.modifier
import android.view.View
import android.view.ViewTreeObserver
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.observeReads
import androidx.compose.ui.platform.LocalView
import dev.serhiiyaremych.imla.ext.logd
import dev.serhiiyaremych.imla.uirenderer.UiLayerRenderer
public fun Modifier.blurSource(uiLayerRenderer: UiLayerRenderer): Modifier {
return this then ImlaSourceElement(uiLayerRenderer)
}
internal class ImlaSourceNode(
internal var uiLayerRenderer: UiLayerRenderer
) : Modifier.Node(), DrawModifierNode, ObserverModifierNode, CompositionLocalConsumerModifierNode {
private var currentView: View? by mutableStateOf(null)
@Suppress("ObjectLiteralToLambda")
private val onDrawListener = object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
uiLayerRenderer.onUiLayerUpdated()
return true
}
}
override fun onAttach() {
super.onAttach()
logd(TAG, "onAttach")
onObservedReadsChanged()
}
private fun subscribeOnDrawListener() {
currentView?.viewTreeObserver?.addOnPreDrawListener(onDrawListener)
}
private fun removeOnDrawListener() {
currentView?.viewTreeObserver?.removeOnPreDrawListener(onDrawListener)
}
override fun onObservedReadsChanged() {
observeReads {
currentView = currentValueOf(LocalView)
subscribeOnDrawListener()
}
}
override fun onDetach() {
super.onDetach()
logd(TAG, "onDetach")
removeOnDrawListener()
}
override fun ContentDrawScope.draw() {
if (drawContext.canvas.nativeCanvas.isHardwareAccelerated) {
uiLayerRenderer.recordCanvas { this@draw.drawContent() }
}
drawContent()
}
companion object {
private const val TAG = "ImlaNode"
}
}
internal class ImlaSourceElement(
private val uiLayerRenderer: UiLayerRenderer
) : ModifierNodeElement() {
override fun create(): ImlaSourceNode {
return ImlaSourceNode(uiLayerRenderer)
}
override fun update(node: ImlaSourceNode) {
node.uiLayerRenderer = uiLayerRenderer
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ImlaSourceElement
return uiLayerRenderer == other.uiLayerRenderer
}
override fun hashCode(): Int {
return uiLayerRenderer.hashCode()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/GfxBuffer.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.renderer
import dev.serhiiyaremych.imla.renderer.opengl.OpenGLUniformBuffer
import dev.serhiiyaremych.imla.renderer.opengl.buffer.OpenGLIndexBuffer
import dev.serhiiyaremych.imla.renderer.opengl.buffer.OpenGLVertexBuffer
import java.nio.Buffer
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import java.nio.IntBuffer
internal interface GfxBuffer {
val elements: Int
val sizeBytes: Int
fun bind()
fun unbind()
fun destroy()
}
internal enum class ShaderDataType(val components: kotlin.Int) {
None(0),
Float(1),
Float2(2),
Float3(3),
Float4(4),
Mat3(3 * 3),
Mat4(4 * 4),
Int(1),
Int2(2),
Int3(3),
Int4(4),
Bool(1);
}
internal data class BufferElement(
val name: String,
val type: ShaderDataType,
val sizeBytes: Int,
var offset: Int = 0,
val normalized: Boolean = false
)
internal class BufferLayout(
val elements: List
) : Iterable by elements {
var stride: Int = 0
private set
constructor(action: MutableList.() -> Unit) : this(buildList(action))
init {
calculateOffsetAndStride()
}
private fun calculateOffsetAndStride() {
var offset = 0
elements.forEach { element ->
element.offset = offset
offset += element.sizeBytes
stride += element.sizeBytes
}
}
}
internal interface VertexBuffer : GfxBuffer {
var layout: BufferLayout?
fun setData(data: FloatArray)
companion object {
fun create(count: Int): VertexBuffer {
return OpenGLVertexBuffer(count)
}
fun create(vertices: FloatArray): VertexBuffer {
return OpenGLVertexBuffer(vertices)
}
}
}
internal interface IndexBuffer : GfxBuffer {
companion object {
fun create(indices: IntArray): IndexBuffer {
return OpenGLIndexBuffer(indices)
}
}
}
internal interface UniformBuffer : GfxBuffer {
fun setData(data: FloatArray)
fun setData(data: Buffer)
companion object {
fun create(count: Int, bindingPoint: Int): UniformBuffer {
return OpenGLUniformBuffer(count, bindingPoint)
}
}
}
internal fun FloatArray.toFloatBuffer(): FloatBuffer = ByteBuffer
.allocateDirect(size * Float.SIZE_BYTES)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(this)
.position(0) as FloatBuffer
internal fun IntArray.toIntBuffer(): IntBuffer = ByteBuffer
.allocateDirect(size * Int.SIZE_BYTES)
.order(ByteOrder.nativeOrder())
.asIntBuffer()
.put(this)
.position(0) as IntBuffer
internal fun MutableList.addElement(
name: String,
type: ShaderDataType,
normalized: Boolean = false
) {
val element = BufferElement(
name = name,
type = type,
sizeBytes = type.sizeBytes,
normalized = normalized
)
add(element)
}
// @formatter:off
internal val ShaderDataType.sizeBytes: Int
get() = when (this) {
ShaderDataType.Float -> Float.SIZE_BYTES * 1
ShaderDataType.Float2 -> Float.SIZE_BYTES * 2
ShaderDataType.Float3 -> Float.SIZE_BYTES * 3
ShaderDataType.Float4 -> Float.SIZE_BYTES * 4
ShaderDataType.Mat3 -> Float.SIZE_BYTES * 3 * 3
ShaderDataType.Mat4 -> Float.SIZE_BYTES * 4 * 4
ShaderDataType.Int -> Int.SIZE_BYTES * 1
ShaderDataType.Int2 -> Int.SIZE_BYTES * 2
ShaderDataType.Int3 -> Int.SIZE_BYTES * 3
ShaderDataType.Int4 -> Int.SIZE_BYTES * 4
ShaderDataType.Bool -> 1
ShaderDataType.None -> 0
}
// @formatter:on
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/RenderCommand.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.renderer
import androidx.compose.ui.graphics.Color
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
import dev.serhiiyaremych.imla.renderer.opengl.OpenGLRendererAPI
internal object RenderCommand {
private val rendererAPI: RendererApi = OpenGLRendererAPI()
val colorBufferBit: Int = rendererAPI.colorBufferBit
val linearTextureFilter: Int = rendererAPI.linearTextureFilter
fun init() {
rendererAPI.init()
}
fun setClearColor(color: Color) {
rendererAPI.setClearColor(color)
}
fun colorMask(red: Boolean, green: Boolean, blue: Boolean, alpha: Boolean) {
rendererAPI.colorMask(red, green, blue, alpha)
}
fun clear() {
rendererAPI.clear()
}
fun clear(color: Color) {
setClearColor(color)
rendererAPI.clear()
}
fun drawIndexed(vertexArray: VertexArray, indexCount: Int = 0) {
rendererAPI.drawIndexed(vertexArray, indexCount)
}
fun setViewPort(x: Int = 0, y: Int = 0, width: Int, height: Int) {
rendererAPI.setViewPort(x, y, width, height)
}
fun disableDepthTest() {
rendererAPI.disableDepthTest()
}
fun enableBlending() {
rendererAPI.enableBlending()
}
fun disableBlending() {
rendererAPI.disableBlending()
}
fun bindDefaultFramebuffer(bind: Bind = Bind.BOTH) {
rendererAPI.bindDefaultFramebuffer(bind)
}
fun useDefaultProgram() = rendererAPI.bindDefaultProgram()
fun blitFramebuffer(
srcX0: Int,
srcY0: Int,
srcX1: Int,
srcY1: Int,
dstX0: Int,
dstY0: Int,
dstX1: Int,
dstY1: Int,
mask: Int = colorBufferBit,
filter: Int = linearTextureFilter,
) {
rendererAPI.blitFramebuffer(
srcX0 = srcX0,
srcY0 = srcY0,
srcX1 = srcX1,
srcY1 = srcY1,
dstX0 = dstX0,
dstY0 = dstY0,
dstX1 = dstX1,
dstY1 = dstY1,
mask = mask,
filter = filter
)
}
inline fun withBlendingModeEnabled(block: () -> Unit) {
enableBlending()
block()
disableBlending()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/Renderer2D.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.renderer
import android.content.res.AssetManager
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.tracing.trace
import dev.romainguy.kotlin.math.Float2
import dev.romainguy.kotlin.math.Float3
import dev.romainguy.kotlin.math.Float4
import dev.romainguy.kotlin.math.Mat4
import dev.romainguy.kotlin.math.rotation
import dev.romainguy.kotlin.math.scale
import dev.romainguy.kotlin.math.translation
import dev.romainguy.kotlin.math.transpose
import dev.serhiiyaremych.imla.renderer.camera.OrthographicCamera
import dev.serhiiyaremych.imla.renderer.objects.QuadShaderProgram
import dev.serhiiyaremych.imla.renderer.primitive.QuadVertex
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import dev.serhiiyaremych.imla.renderer.shader.ShaderProgram
internal const val MAX_QUADS = 50
internal const val MAX_VERTICES = MAX_QUADS * 4
internal const val MAX_INDICES = MAX_QUADS * 6
internal const val MAX_TEXTURE_SLOTS = 8 // query from actual HW
internal class Renderer2D {
private var _data: Renderer2DData? = null
internal val data: Renderer2DData get() = _data ?: error("Renderer2D not initialized!")
private var isDrawingScene: Boolean = false
fun init(shaderLibrary: ShaderLibrary, shaderBinder: ShaderBinder) {
val quadIndices = IntArray(MAX_INDICES)
var offset = 0
for (i in quadIndices.indices step 6) {
quadIndices[i + 0] = offset + 0
quadIndices[i + 1] = offset + 1
quadIndices[i + 2] = offset + 2
quadIndices[i + 3] = offset + 2
quadIndices[i + 4] = offset + 3
quadIndices[i + 5] = offset + 0
offset += 4
}
val quadVertexArray: VertexArray = VertexArray.create()
val defaultQuadShaderProgram = QuadShaderProgram(
shaderBinder,
shader = shaderLibrary.loadShaderFromFile(
vertFileName = "default_quad",
fragFileName = "default_quad"
)
)
val quadVertexBuffer: VertexBuffer =
VertexBuffer.create(MAX_VERTICES * defaultQuadShaderProgram.componentsCount).apply {
layout = defaultQuadShaderProgram.vertexBufferLayout
}
quadVertexArray.addVertexBuffer(quadVertexBuffer)
quadVertexArray.indexBuffer = IndexBuffer.create(quadIndices)
val externalQuadShaderProgram = QuadShaderProgram(
shaderBinder,
shader = shaderLibrary.loadShaderFromFile(
vertFileName = "default_quad",
fragFileName = "external_quad"
)
)
val quadIndexCount = 0
val quadVertexBufferBase: MutableList = ArrayList(MAX_VERTICES)
val defaultQuadVertexPositions: Array = Array(4) {
when (it) {
0 -> Float4(-0.5f, -0.5f, 0.0f, 1.0f) // BL
1 -> Float4(0.5f, -0.5f, 0.0f, 1.0f) // BR
2 -> Float4(0.5f, 0.5f, 0.0f, 1.0f) // TR
3 -> Float4(-0.5f, 0.5f, 0.0f, 1.0f) // TL
else -> error("Wrong QuadVertex index: $it, max 3")
}
}
val stats = RenderStatistics()
val whiteTexture: Texture2D =
Texture2D.create(Texture.Target.TEXTURE_2D, Texture.Specification())
val textureSlots: Array = Array(MAX_TEXTURE_SLOTS) { null }
val textureSlotIndex = 1 // 0 = white texture slot
_data = Renderer2DData(
cameraData = CameraData(Mat4.identity()),
whiteTexture = whiteTexture,
setStaticQuadData = false,
quadVertexArray = quadVertexArray,
quadVertexBuffer = quadVertexBuffer,
defaultQuadShaderProgram = defaultQuadShaderProgram,
externalQuadShaderProgram = externalQuadShaderProgram,
quadShaderProgram = externalQuadShaderProgram,
quadIndexCount = quadIndexCount,
quadVertexBufferBase = quadVertexBufferBase,
textureSlots = textureSlots,
quadVertexBufferStatic = null,
textureSlotIndex = textureSlotIndex,
defaultQuadVertexPositions = defaultQuadVertexPositions,
stats = stats,
shaderBinder = shaderBinder
)
whiteTexture.setData(intArrayOf(Color.White.toArgb()).toIntBuffer())
externalQuadShaderProgram.shader.bind(shaderBinder)
textureSlots.fill(null)
textureSlots[WHITE_TEXTURE_SLOT_INDEX] = whiteTexture
}
fun shutdown() {
data.defaultQuadShaderProgram.destroy()
data.externalQuadShaderProgram.destroy()
data.quadShaderProgram.destroy()
data.quadVertexArray.destroy()
data.textureSlots.fill(null)
_data = null
}
fun beginScene(camera: OrthographicCamera) {
beginScene(camera, data.defaultQuadShaderProgram)
}
fun beginScene(camera: OrthographicCamera, shaderProgram: ShaderProgram) =
trace("Renderer2D#beginScene") {
require(isDrawingScene.not()) { "Please complete the current scene before starting a new one." }
isDrawingScene = true
data.cameraData.viewProjection = camera.viewProjectionMatrix
if (shaderProgram != data.quadShaderProgram) {
data.quadShaderProgram = shaderProgram
}
val mat4 = data.cameraData.viewProjection
trace("viewProjection") {
data.defaultQuadShaderProgram.shader.bind(data.shaderBinder)
data.defaultQuadShaderProgram.shader.setMat4("u_ViewProjection", mat4)
data.externalQuadShaderProgram.shader.bind(data.shaderBinder)
data.externalQuadShaderProgram.shader.setMat4("u_ViewProjection", mat4)
data.quadShaderProgram.shader.bind(data.shaderBinder)
data.quadShaderProgram.shader.setMat4("u_ViewProjection", mat4)
}
data.quadIndexCount = 0
data.quadVertexBufferBase.clear()
data.textureSlotIndex = 1
data.textureSlots.fill(null, 1)
}
fun endScene() = trace("Renderer2D#endScene") {
data.quadVertexBuffer.setData(
data.quadShaderProgram.mapVertexData(data.quadVertexBufferBase)
)
flush()
isDrawingScene = false
}
private fun flush() = trace("flush") {
if (data.quadIndexCount > 0) {
// Bind textures
for (i in 0 until data.textureSlotIndex) {
data.textureSlots[i]?.bind(slot = i)
}
val isCustomShader = data.quadShaderProgram != data.defaultQuadShaderProgram &&
data.quadShaderProgram != data.externalQuadShaderProgram
when {
isCustomShader -> {
data.quadShaderProgram.shader.bind(data.shaderBinder)
}
else -> {
if (data.quadVertexBufferBase.any { it.isExternalTexture > 0f }) {
data.externalQuadShaderProgram.shader.bind(data.shaderBinder)
} else {
data.defaultQuadShaderProgram.shader.bind(data.shaderBinder)
}
}
}
RenderCommand.drawIndexed(data.quadVertexArray, data.quadIndexCount)
data.stats.drawCalls++
}
}
fun drawFullQuadStatic(texture: Texture, alpha: Float) = trace("drawFullQuadStatic") {
if (data.setStaticQuadData.not()) {
submitQuad(
transform = matId,
textureCoords = if (texture is SubTexture2D) texture.texCoords else defaultTextureCoords,
alpha = alpha,
mask = 0f
)
data.quadVertexBufferStatic = VertexBuffer.create(
vertices = data.quadShaderProgram.mapVertexData(data.quadVertexBufferBase)
)
data.quadVertexArray.addVertexBuffer(data.quadVertexBufferStatic!!)
data.setStaticQuadData = true
data.quadShaderProgram.shader.bind(data.shaderBinder)
data.quadShaderProgram.shader.setFloat("staticRenderer", 1.0f)
}
flush()
isDrawingScene = false
}
fun drawQuad(
position: Float3,
size: Float2,
rotated: Float3 = zero3,
cameraDistance: Float = 3f,
texture2D: Texture2D? = null,
alpha: Float = 1.0f,
withMask: Boolean = false
) = trace("drawQuad") {
if (data.quadIndexCount >= MAX_INDICES) {
flushAndReset()
}
val transform: Mat4
if (rotated != zero3) {
val depth = cameraDistance * 72f
var cameraDepth = matId
if (rotated.x != 0f || rotated.y != 0f) { // faking perspective mapping
cameraDepth = Mat4.identity()
cameraDepth.set(row = 2, column = 3, v = -1f / depth)
cameraDepth.set(row = 2, column = 2, v = 0f)
cameraDepth = transpose(cameraDepth)
}
val rotX = if (rotated.x != 0.0f) rotation(X_AXIS, rotated.x) else matId
val rotY = if (rotated.y != 0.0f) rotation(Y_AXIS, rotated.y) else matId
val rotZ = if (rotated.z != 0.0f) rotation(Z_AXIS, rotated.z) else matId
transform = translation(position) *
cameraDepth *
rotX * rotY * rotZ *
scale(Float3(size, 1.0f))
} else {
transform = translation(position) * scale(Float3(size, 1.0f))
}
var texIndex = 0.0f
var flipTexture = false
var isExternalTexture = false
if (texture2D != null) {
flipTexture = texture2D.flipTexture
isExternalTexture = texture2D.target == Texture.Target.TEXTURE_EXTERNAL_OES
var textureIndex = findTextureSlotIndexFor(texture2D)
if (textureIndex == -1) {
textureIndex = data.textureSlotIndex++
} else {
data.textureSlotIndex = textureIndex + 1
}
data.textureSlots[textureIndex] = texture2D
texIndex = textureIndex.toFloat()
}
submitQuad(
transform = transform,
texIndex = texIndex,
flipTexture = if (flipTexture) 1.0f else 0.0f,
isExternalTexture = if (isExternalTexture) 1.0f else 0.0f,
alpha = alpha,
mask = if (withMask) 1.0f else 0.0f
)
}
fun drawQuad(
position: Float3,
size: Float2,
texture: Texture,
alpha: Float = 1.0f,
withMask: Boolean = false
) = trace("drawQuad[${size.x.toInt()} x ${size.y.toInt()}]") {
when (texture) {
is Texture2D -> drawQuad(
position = position,
size = size,
texture2D = texture,
alpha = alpha,
withMask = withMask
)
is SubTexture2D -> drawQuad(
position = position,
size = size,
subTexture = texture,
alpha = alpha,
withMask = withMask
)
}
Unit
}
fun drawQuad(
position: Float3,
size: Float2,
subTexture: SubTexture2D,
alpha: Float = 1.0f,
withMask: Boolean = false
) {
if (data.quadIndexCount >= MAX_INDICES) {
flushAndReset()
}
var textureIndex = findTextureSlotIndexFor(subTexture.texture)
if (textureIndex == -1) {
textureIndex = data.textureSlotIndex
data.textureSlots[textureIndex] = subTexture.texture
data.textureSlotIndex++
} else {
data.textureSlotIndex = textureIndex + 1
}
val isExternalTexture = subTexture.texture.target == Texture.Target.TEXTURE_EXTERNAL_OES
submitQuad(
transform = translation(position) * scale(Float3(size, 1.0f)),
texIndex = textureIndex.toFloat(),
textureCoords = subTexture.texCoords,
flipTexture = if (subTexture.flipTexture) 1.0f else 0.0f,
isExternalTexture = if (isExternalTexture) 1.0f else 0.0f,
alpha = alpha,
mask = if (withMask) 1.0f else 0.0f
)
}
fun resetRenderStats() {
data.stats.reset()
}
fun renderStats(): RenderStatistics {
return if (_data != null) data.stats else RenderStatistics()
}
private fun flushAndReset() {
endScene()
data.quadIndexCount = 0
data.quadVertexBufferBase.clear()
data.textureSlotIndex = 1
data.textureSlots.fill(null, 1)
data.textureSlots[WHITE_TEXTURE_SLOT_INDEX] = data.whiteTexture
}
private fun findTextureSlotIndexFor(texture: Texture2D): Int {
var textureIndex = -1
for (i in 1..data.textureSlotIndex) {
if (data.textureSlots[i] == texture) {
textureIndex = i
}
}
return textureIndex
}
private fun submitQuad(
transform: Mat4,
textureCoords: Array = defaultTextureCoords,
texIndex: Float = 0.0f, // 0 = white texture
flipTexture: Float = 1.0f,
isExternalTexture: Float = 0.0f,
alpha: Float = 1.0f,
mask: Float
) = trace("submitQuad") {
for (i in 0 until 4) {
data.quadVertexBufferBase += QuadVertex(
position = (transform * data.defaultQuadVertexPositions[i]),
texCoord = textureCoords[i],
texIndex = texIndex,
flipTexture = flipTexture,
isExternalTexture = isExternalTexture,
alpha = alpha,
mask = mask,
maskCoord = defaultTextureCoords[i]
)
}
data.quadIndexCount += 6
data.stats.quadCount++
}
}
internal data class CameraData(var viewProjection: Mat4)
@Suppress("ArrayInDataClass")
internal data class Renderer2DData(
val cameraData: CameraData,
var whiteTexture: Texture2D,
var setStaticQuadData: Boolean,
val defaultQuadShaderProgram: ShaderProgram,
val externalQuadShaderProgram: ShaderProgram,
var quadVertexArray: VertexArray,
var quadVertexBuffer: VertexBuffer,
var quadShaderProgram: ShaderProgram,
var quadIndexCount: Int = 0,
val quadVertexBufferBase: MutableList = ArrayList(MAX_VERTICES),
val textureSlots: Array = Array(MAX_TEXTURE_SLOTS) { null },
var quadVertexBufferStatic: VertexBuffer?,
var textureSlotIndex: Int = 1, // 0 = white texture slot
val defaultQuadVertexPositions: Array = Array(4) { Float4(0.0f) },
val stats: RenderStatistics = RenderStatistics(),
val shaderBinder: ShaderBinder,
) {
}
public data class RenderStatistics(
var drawCalls: Int = 0,
var quadCount: Int = 0,
var lineCount: Int = 0,
var frameTime: Float = 0f,
) {
val vertexCount: Int get() = (quadCount * 4) + (lineCount * 4)
val indexCount: Int get() = quadCount * 6 + (lineCount * 6)
public fun reset() {
drawCalls = 0
quadCount = 0
lineCount = 0
frameTime = 0f
}
}
// Texture coordinates
private val bottomLeft = Offset(0.0f, 0.0f)
private val bottomRight = Offset(1.0f, 0.0f)
private val topRight = Offset(1.0f, 1.0f)
private val topLeft = Offset(0.0f, 1.0f)
private val defaultTextureCoords = arrayOf(bottomLeft, bottomRight, topRight, topLeft) // CCW
//private val defaultTextureCoords = arrayOf(
// Offset(0.0f, 0.0f), // Bottom Left
// Offset(1.0f, 0.0f), // Bottom Right
// Offset(0.0f, 1.0f), // Top Left
// Offset(1.0f, 1.0f) // Top Right
//)
private val whiteColor = Float4(1.0f)
private val zero3 = Float3(0.0f)
private val zero4 = Float4(0.0f)
private val X_AXIS = Float3(1.0f, 0.0f, 0.0f)
private val Y_AXIS = Float3(0.0f, 1.0f, 0.0f)
private val Z_AXIS = Float3(0.0f, 0.0f, 1.0f)
private val matId = Mat4.identity()
private const val WHITE_TEXTURE_SLOT_INDEX = 0
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/RendererApi.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.renderer
import androidx.compose.ui.graphics.Color
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
internal interface RendererApi {
val colorBufferBit: Int
val linearTextureFilter: Int
enum class Api { None, OpenGL }
fun init()
fun setClearColor(color: Color)
fun clear()
fun drawIndexed(vertexArray: VertexArray, indexCount: Int = 0)
fun setViewPort(x: Int, y: Int, width: Int, height: Int)
fun disableDepthTest()
fun colorMask(red: Boolean, green: Boolean, blue: Boolean, alpha: Boolean)
fun enableBlending()
fun disableBlending()
fun bindDefaultFramebuffer(bind: Bind)
fun bindDefaultProgram()
fun blitFramebuffer(
srcX0: Int,
srcY0: Int,
srcX1: Int,
srcY1: Int,
dstX0: Int,
dstY0: Int,
dstX1: Int,
dstY1: Int,
mask: Int,
filter: Int,
)
companion object {
internal val api: Api = Api.OpenGL
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/SimpleRenderer.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer
import androidx.tracing.trace
internal class SimpleRenderer {
internal val data: StaticRendererData
get() = _data ?: error("StaticRenderer not initialised!")
private var _data: StaticRendererData? = null
fun init() {
// vec2 uv[4]; // x, y
// vec2 size; // width, height
// float flipTexture; //
// float alpha; //
val texDataUboElements = 20
val textureDataUBO = UniformBuffer.create(
count = texDataUboElements,
bindingPoint = TEXTURE_DATA_UBO_BINDING_POINT
)
val rendererData = StaticRendererData(
textureDataUBO = textureDataUBO,
vao = VertexArray.create(),
)
rendererData.vao.bind()
rendererData.vao.indexBuffer = allocateIndexBuffer(indices = 6)
rendererData.vao.addVertexBuffer(allocateVertexBuffer())
_data = rendererData
}
private fun allocateVertexBuffer(): VertexBuffer {
return VertexBuffer.create(
// @formatter:off
floatArrayOf(
-1.0f, -1.0f, // bottom left
1.0f, -1.0f, // bottom right
1.0f, 1.0f, // top right
-1.0f, 1.0f, // top left
)
// @formatter:on
).apply {
layout = BufferLayout {
addElement("aPosition", ShaderDataType.Float2)
}
}
}
private fun allocateIndexBuffer(indices: Int = MAX_INDICES): IndexBuffer {
val quadIndices = IntArray(indices)
var offset = 0
// simple quad indices
for (i in quadIndices.indices step 6) {
quadIndices[i + 0] = offset + 0
quadIndices[i + 1] = offset + 1
quadIndices[i + 2] = offset + 2
quadIndices[i + 3] = offset + 2
quadIndices[i + 4] = offset + 3
quadIndices[i + 5] = offset + 0
offset += 4
}
return IndexBuffer.create(quadIndices)
}
fun flush() = trace("SimpleRenderer#flush") {
RenderCommand.drawIndexed(data.vao, 6)
}
companion object {
const val TEXTURE_DATA_UBO_BLOCK = "TextureDataUBO"
const val TEXTURE_DATA_UBO_BINDING_POINT = 0
}
}
internal class StaticRendererData(
val textureDataUBO: UniformBuffer,
val vao: VertexArray,
)
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/SubTexture2D.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.renderer
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toIntSize
import dev.serhiiyaremych.imla.uirenderer.processing.SimpleQuadRenderer
internal class SubTexture2D(
val texture: Texture2D
) : Texture by texture {
val texCoords: Array = Array(4) { index ->
SimpleQuadRenderer.defaultTextureCoords[index]
}
var subTextureSize: IntSize = IntSize.Zero
private set
constructor(texture: Texture2D, min: Offset, max: Offset) : this(texture) {
texCoords[0] = min
texCoords[1] = Offset(max.x, min.y)
texCoords[2] = Offset(max.x, max.y)
texCoords[3] = Offset(min.x, max.y)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SubTexture2D
if (texture != other.texture) return false
if (!texCoords.contentEquals(other.texCoords)) return false
if (subTextureSize != other.subTextureSize) return false
return true
}
override fun hashCode(): Int {
var result = texture.hashCode()
result = 31 * result + texCoords.contentHashCode()
result = 31 * result + subTextureSize.hashCode()
return result
}
companion object {
fun createFromCoords(
texture: Texture2D,
rect: Rect
): SubTexture2D {
val texLeft: Float = rect.left / texture.width
val texRight: Float = (rect.right) / texture.width
val texTop: Float = 1.0f - (rect.top / texture.height)
val texBottom: Float = 1.0f - ((rect.bottom) / texture.height)
val min = Offset(x = texLeft, y = texTop)
val max = Offset(x = texRight, y = texBottom)
return SubTexture2D(texture = texture, min = min, max = max).apply {
subTextureSize = rect.size.toIntSize()
}
}
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/Texture.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.renderer
import androidx.compose.ui.unit.IntSize
import dev.serhiiyaremych.imla.renderer.opengl.OpenGLTexture2D
import java.nio.Buffer
internal interface Texture {
enum class Target {
TEXTURE_2D,
TEXTURE_EXTERNAL_OES,
//TEXTURE_2D_ARRAY // do I need it?
}
enum class ImageFormat {
None,
A8,
R8,
R16F,
RGB8,
RGBA8,
RGB10_A2,
DEPTH24STENCIL8
}
data class Specification(
val size: IntSize = IntSize(1, 1),
val format: ImageFormat = ImageFormat.RGBA8,
val generateMips: Boolean = false,
var flipTexture: Boolean = false,
val mipmapFiltering: Boolean = false
)
val id: Int
val target: Target
val width: Int
val height: Int
val flipTexture: Boolean
val specification: Specification
fun bind(slot: Int = 0)
fun setData(data: Buffer)
fun isLoaded(): Boolean
fun destroy()
}
internal abstract class Texture2D : Texture {
companion object {
fun create(target: Texture.Target, specification: Texture.Specification): Texture2D {
return OpenGLTexture2D(target, specification)
}
fun create(
target: Texture.Target,
textureId: Int,
specification: Texture.Specification
): Texture2D {
return OpenGLTexture2D(textureId, target, specification)
}
}
abstract fun generateMipMaps()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Texture2D
return id == other.id
}
override fun hashCode(): Int {
return id.hashCode()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/VertexArray.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer
import dev.serhiiyaremych.imla.renderer.opengl.OpenGLVertexArray
internal interface VertexArray {
fun bind()
fun unbind()
fun destroy()
fun addVertexBuffer(vertexBuffer: VertexBuffer)
var indexBuffer: IndexBuffer?
companion object {
fun create(): VertexArray {
return OpenGLVertexArray()
}
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/camera/OrthographicCamera.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.camera
import dev.romainguy.kotlin.math.Float3
import dev.romainguy.kotlin.math.Mat4
import dev.romainguy.kotlin.math.inverse
import dev.romainguy.kotlin.math.ortho
import dev.romainguy.kotlin.math.rotation
import dev.romainguy.kotlin.math.translation
internal class OrthographicCamera(
left: Float, right: Float, bottom: Float, top: Float
) {
var position: Float3 = Float3(0.0f)
set(value) {
field = value
recalculateViewMatrix()
}
var rotation: Float = 0.0f
set(value) {
field = value
recalculateViewMatrix()
}
var projectionMatrix: Mat4 = Mat4.identity()
private set
var viewMatrix: Mat4 = Mat4.identity()
private set
var viewProjectionMatrix: Mat4 = Mat4.identity()
private set
init {
projectionMatrix = ortho(left, right, bottom, top, -1.0f, 1.0f)
viewProjectionMatrix = projectionMatrix * viewMatrix
}
fun setProjection(left: Float, right: Float, bottom: Float, top: Float) {
projectionMatrix = ortho(left, right, bottom, top, -1.0f, 1.0f)
viewProjectionMatrix = projectionMatrix * viewMatrix
}
private fun recalculateViewMatrix() {
val transform = translation(position) * rotation(Float3(0.0f, 0.0f, 1.0f), rotation)
viewMatrix = inverse(transform)
viewProjectionMatrix = projectionMatrix * viewMatrix
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/camera/OrthographicCameraController.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.renderer.camera
internal class OrthographicCameraController {
var camera: OrthographicCamera
private set
var aspectRatio: Float = 1.0f
private set
var zoomLevel: Float = 1.0f
private var viewPortWidth: Int = 0
private var viewPortHeight: Int = 0
private var pixelCoordinates: Boolean = false
val orthographicSize: Float
get() = viewPortHeight / 2f / zoomLevel
private constructor(aspectRatio: Float, height: Int) {
this.aspectRatio = aspectRatio
this.viewPortHeight = height
camera = OrthographicCamera(
left = -aspectRatio * orthographicSize,
right = aspectRatio * orthographicSize,
bottom = -orthographicSize,
top = orthographicSize
)
onVisibleBoundsResize(width = 0, height = height)
}
private constructor(width: Int, height: Int) {
pixelCoordinates = true
camera = OrthographicCamera(
left = 0f,
right = width.toFloat(),
bottom = 0f,
top = height.toFloat()
)
onVisibleBoundsResize(width, height)
}
fun onVisibleBoundsResize(width: Int, height: Int) {
if (width != viewPortWidth || height != viewPortHeight) {
viewPortWidth = width
viewPortHeight = height
if (width != 0 && height != 0) {
aspectRatio = width / height.toFloat()
}
updateCameraProjection()
}
}
fun updateCameraProjection() {
if (pixelCoordinates) {
camera.setProjection(
left = 0f,
right = viewPortWidth.toFloat(),
bottom = 0f,
top = viewPortHeight.toFloat()
)
} else {
camera.setProjection(
left = -aspectRatio * orthographicSize,
right = aspectRatio * orthographicSize,
bottom = -orthographicSize,
top = orthographicSize
)
}
}
companion object {
fun createPixelUnitsController(
viewportWidth: Int,
viewportHeight: Int
) = OrthographicCameraController(viewportWidth, viewportHeight)
fun createWorldUnitsController(
viewportWidth: Int,
viewportHeight: Int
) = OrthographicCameraController(viewportWidth / viewportHeight.toFloat(), viewportHeight)
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/framebuffer/BumpAllocatorPool.kt
================================================
package dev.serhiiyaremych.imla.renderer.framebuffer
internal class BumpAllocatorPool {
val items = ArrayList()
var length: Int = 0
inline fun acquire(factory: () -> T): T {
if (length >= items.size) {
items.add(factory())
}
return items[length++]
}
fun resetPool() {
length = 0
}
fun onEach(onEach: (T) -> Unit) {
items.forEach(onEach)
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/framebuffer/FrameBuffer.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.renderer.framebuffer
import androidx.compose.ui.unit.IntSize
import dev.serhiiyaremych.imla.renderer.Texture2D
import dev.serhiiyaremych.imla.renderer.opengl.buffer.OpenGLFramebuffer
internal enum class Bind {
READ, DRAW, BOTH
}
internal enum class FramebufferTextureFormat {
// bw masks, noise
R8,
// Color
RGBA8,
RGB10_A2,
// Depth/stencil
DEPTH24STENCIL8,
}
internal data class FramebufferTextureSpecification(
val format: FramebufferTextureFormat = FramebufferTextureFormat.RGBA8,
val flip: Boolean = false,
val mipmapFiltering: Boolean = false
)
internal data class FramebufferAttachmentSpecification(
val attachments: List = listOf(
FramebufferTextureSpecification(format = FramebufferTextureFormat.RGBA8),
// FramebufferTextureSpecification(format = FramebufferTextureFormat.RGB10_A2),
)
) {
companion object {
fun singleColor(
format: FramebufferTextureFormat = FramebufferTextureFormat.RGBA8,
mipmapFiltering: Boolean = false,
flip: Boolean = false
): FramebufferAttachmentSpecification {
return FramebufferAttachmentSpecification(
attachments = listOf(
FramebufferTextureSpecification(
format = format,
mipmapFiltering = mipmapFiltering,
flip = flip
)
)
)
}
}
}
internal data class FramebufferSpecification(
val size: IntSize,
val attachmentsSpec: FramebufferAttachmentSpecification,
val downSampleFactor: Int = 1,
)
internal interface Framebuffer {
val rendererId: Int
val specification: FramebufferSpecification
val colorAttachmentTexture: Texture2D
fun invalidate()
fun bind(bind: Bind = Bind.BOTH, updateViewport: Boolean = true)
fun unbind()
fun resize(width: Int, height: Int)
fun invalidateAttachments()
fun clearAttachment(attachmentIndex: Int, value: Int)
fun getColorAttachmentRendererID(index: Int = 0): Int
fun destroy()
fun setColorAttachmentAt(attachmentIndex: Int)
companion object {
fun create(spec: FramebufferSpecification): Framebuffer {
return OpenGLFramebuffer(spec)
}
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/framebuffer/FramebufferPool.kt
================================================
package dev.serhiiyaremych.imla.renderer.framebuffer
import androidx.compose.ui.graphics.Color
import dev.serhiiyaremych.imla.renderer.RenderCommand
internal class FramebufferPool {
private val frameBuffers = mutableMapOf>()
fun acquire(spec: FramebufferSpecification): Framebuffer {
val existing = frameBuffers.getOrPut(spec) { BumpAllocatorPool() }
return existing.acquire { Framebuffer.create(spec) }
}
fun resetPool() {
frameBuffers.values.forEach { it.resetPool() }
}
fun eraseAll() {
frameBuffers.values.forEach {
it.onEach {
it.bind(updateViewport = false)
RenderCommand.clear(Color.Transparent)
it.unbind()
}
}
}
fun clear() {
frameBuffers.values.forEach { it.onEach { it.destroy() } }
frameBuffers.clear()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/objects/QuadShaderProgram.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.objects
import androidx.tracing.trace
import dev.serhiiyaremych.imla.renderer.BufferLayout
import dev.serhiiyaremych.imla.renderer.MAX_TEXTURE_SLOTS
import dev.serhiiyaremych.imla.renderer.shader.Shader
import dev.serhiiyaremych.imla.renderer.ShaderDataType
import dev.serhiiyaremych.imla.renderer.shader.ShaderProgram
import dev.serhiiyaremych.imla.renderer.addElement
import dev.serhiiyaremych.imla.renderer.primitive.QuadVertex
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
internal val defaultQuadBufferLayout = BufferLayout {
addElement("a_TexCoord", ShaderDataType.Float2)
addElement("a_Position", ShaderDataType.Float4)
addElement("a_TexIndex", ShaderDataType.Float)
addElement("a_FlipTexture", ShaderDataType.Float)
addElement("a_IsExternalTexture", ShaderDataType.Float)
addElement("a_Alpha", ShaderDataType.Float)
addElement("a_Mask", ShaderDataType.Float)
addElement("a_MaskCoord", ShaderDataType.Float2)
}
internal fun defaultQuadVertexMapper(
quadVertexBufferBase: List
): FloatArray = trace("defaultQuadVertexMapper") {
// TODO: Use Bytebuffer to ensure vertex data is aligned
val verticesData =
FloatArray(quadVertexBufferBase.count() * QuadVertex.NUMBER_OF_COMPONENTS)
var lastVertexIndex = 0
for (quad in quadVertexBufferBase) {
// a_TexCoord
verticesData[lastVertexIndex + 0] = quad.texCoord.x
verticesData[lastVertexIndex + 1] = quad.texCoord.y
// a_Position
verticesData[lastVertexIndex + 2] = quad.position.x
verticesData[lastVertexIndex + 3] = quad.position.y
verticesData[lastVertexIndex + 4] = quad.position.z
verticesData[lastVertexIndex + 5] = quad.position.w
// a_TexIndex
verticesData[lastVertexIndex + 6] = quad.texIndex
// a_FlipTexture
verticesData[lastVertexIndex + 7] = quad.flipTexture
// a_IsExternalTexture
verticesData[lastVertexIndex + 8] = quad.isExternalTexture
// a_Alpha
verticesData[lastVertexIndex + 9] = quad.alpha
// a_Mask
verticesData[lastVertexIndex + 10] = quad.mask
// a_MaskCoord
verticesData[lastVertexIndex + 11] = quad.maskCoord.x
verticesData[lastVertexIndex + 12] = quad.maskCoord.y
lastVertexIndex += QuadVertex.NUMBER_OF_COMPONENTS
}
return verticesData
}
internal class QuadShaderProgram(
shaderBinder: ShaderBinder,
override val shader: Shader,
textureSlots: Int = MAX_TEXTURE_SLOTS
) : ShaderProgram {
override val vertexBufferLayout: BufferLayout = defaultQuadBufferLayout
override val componentsCount: Int = vertexBufferLayout.elements.sumOf { it.type.components }
init {
shader.bind(shaderBinder)
val samplers = IntArray(textureSlots) { index -> index }
shader.setIntArray("u_Textures", samplers)
}
override fun mapVertexData(quadVertexBufferBase: List): FloatArray {
return defaultQuadVertexMapper(quadVertexBufferBase)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as QuadShaderProgram
return shader == other.shader
}
override fun hashCode(): Int {
return shader.hashCode()
}
override fun destroy() {
shader.destroy()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/opengl/OpenGLRendererApi.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.opengl
import android.opengl.GLES30
import android.util.Log
import androidx.compose.ui.graphics.Color
import androidx.tracing.trace
import dev.serhiiyaremych.imla.ext.checkGlError
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
import dev.serhiiyaremych.imla.renderer.RendererApi
import dev.serhiiyaremych.imla.renderer.VertexArray
import dev.serhiiyaremych.imla.renderer.opengl.buffer.toGlTarget
internal class OpenGLRendererAPI : RendererApi {
override val colorBufferBit: Int = GLES30.GL_COLOR_BUFFER_BIT
override val linearTextureFilter: Int = GLES30.GL_LINEAR
override fun init() {
// LOG
Log.d(TAG, "vendor: " + GLES30.glGetString(GLES30.GL_VENDOR))
Log.d(TAG, "renderer: " + GLES30.glGetString(GLES30.GL_RENDERER))
Log.d(TAG, "version: " + GLES30.glGetString(GLES30.GL_VERSION))
checkGlError(GLES30.glBlendFunc(GLES30.GL_SRC_ALPHA, GLES30.GL_ONE_MINUS_SRC_ALPHA))
checkGlError(GLES30.glDisable(GLES30.GL_DEPTH_TEST))
checkGlError(GLES30.glDisable(GLES30.GL_STENCIL_TEST))
checkGlError(GLES30.glDisable(GLES30.GL_SCISSOR_TEST))
checkGlError(GLES30.glDisable(GLES30.GL_CULL_FACE))
setClearColor(Color.Transparent)
}
override fun setClearColor(color: Color) = trace("setClearColor") {
checkGlError(GLES30.glClearColor(color.red, color.green, color.blue, color.alpha))
}
override fun clear() = trace("glClear") {
checkGlError(GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT or GLES30.GL_DEPTH_BUFFER_BIT or GLES30.GL_STENCIL_BUFFER_BIT))
}
// @formatter:off
override fun drawIndexed(vertexArray: VertexArray, indexCount: Int) = trace("drawIndexed") {
vertexArray.bind()
GLES30.glDrawElements(
/* mode = */ GLES30.GL_TRIANGLES,
/* count = */ if (indexCount == 0) requireNotNull(vertexArray.indexBuffer).elements else indexCount,
/* type = */ GLES30.GL_UNSIGNED_INT,
/* offset = */ 0
)
}
// @formatter:on
override fun setViewPort(x: Int, y: Int, width: Int, height: Int) = trace("glViewport") {
checkGlError(GLES30.glViewport(x, y, width, height))
}
override fun disableDepthTest() {
GLES30.glDisable(GLES30.GL_DEPTH_TEST)
}
override fun colorMask(red: Boolean, green: Boolean, blue: Boolean, alpha: Boolean) {
// @formatter:off
GLES30.glColorMask(
/* red = */ red,
/* green = */ green,
/* blue = */ blue,
/* alpha = */ alpha
)
// @formatter:on
}
override fun enableBlending() = trace("enableBlending") {
GLES30.glEnable(GLES30.GL_BLEND)
}
override fun disableBlending() = trace("disableBlending") {
GLES30.glDisable(GLES30.GL_BLEND)
}
override fun bindDefaultFramebuffer(bind: Bind) = trace("bindDefaultFBO") {
checkGlError(GLES30.glBindFramebuffer(bind.toGlTarget(), 0))
}
override fun bindDefaultProgram() = trace("useDefaultProgram") {
checkGlError(GLES30.glUseProgram(0))
}
override fun blitFramebuffer(
srcX0: Int,
srcY0: Int,
srcX1: Int,
srcY1: Int,
dstX0: Int,
dstY0: Int,
dstX1: Int,
dstY1: Int,
mask: Int,
filter: Int
) = trace("glBlitFramebuffer") {
// @formatter:off
checkGlError(
GLES30.glBlitFramebuffer(
/* srcX0 = */ srcX0,
/* srcY0 = */ srcY0,
/* srcX1 = */ srcX1,
/* srcY1 = */ srcY1,
/* dstX0 = */ dstX0,
/* dstY0 = */ dstY0,
/* dstX1 = */ dstX1,
/* dstY1 = */ dstY1,
/* mask = */ mask,
/* filter = */ filter
)
)
// @formatter:on
}
companion object {
private const val TAG = "OpenGLRendererAPI"
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/opengl/OpenGLShader.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.opengl
import android.opengl.GLES30
import android.util.Log
import androidx.collection.MutableObjectFloatMap
import androidx.collection.MutableObjectIntMap
import androidx.compose.ui.util.trace
import dev.romainguy.kotlin.math.Float2
import dev.romainguy.kotlin.math.Float3
import dev.romainguy.kotlin.math.Float4
import dev.romainguy.kotlin.math.Mat3
import dev.romainguy.kotlin.math.Mat4
import dev.serhiiyaremych.imla.ext.checkGlError
import dev.serhiiyaremych.imla.renderer.shader.Shader
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.stats.ShaderStats
import org.intellij.lang.annotations.Language
internal class OpenGLShader(
name: String,
@Language("GLSL")
vertexSrc: String,
@Language("GLSL")
fragmentSrc: String
) : Shader {
private var _name: String = name
override val name: String get() = _name
private var rendererId: Int = 0
private val locationMap: MutableObjectIntMap = MutableObjectIntMap()
private val intValueCache: MutableObjectIntMap = MutableObjectIntMap()
private val intArrayValueCache: MutableMap = mutableMapOf()
private val floatValueCache: MutableObjectFloatMap = MutableObjectFloatMap()
private val floatArrayValueCache: MutableMap = mutableMapOf()
private var traceName = "shaderBind"
init {
compile(vertexSrc, fragmentSrc)
ShaderStats.shaderInstances++
}
@Deprecated("")
override fun bind() {
trace(traceName) {
checkGlError(GLES30.glUseProgram(rendererId))
}
ShaderStats.shaderBinds++
}
override fun bind(shaderBinder: ShaderBinder) {
shaderBinder.bind(this)
}
override fun unbind() = trace(traceName) {
GLES30.glUseProgram(0)
intValueCache.clear()
intArrayValueCache.clear()
floatValueCache.clear()
floatArrayValueCache.clear()
}
override fun bindUniformBlock(blockName: String, bindingPoint: Int) {
trace("bindUniformBlock") {
val blockIndex =
locationMap.getOrPut(blockName) {
GLES30.glGetUniformBlockIndex(
rendererId,
blockName
).also { checkGlError() }
}
checkGlError(GLES30.glUniformBlockBinding(rendererId, blockIndex, bindingPoint))
}
ShaderStats.shaderBindUniformBlock++
}
override fun setInt(name: String, value: Int) = trace("setInt") {
uploadUniformInt(name, value)
}
override fun setIntArray(name: String, values: IntArray) = trace("setIntArray") {
uploadUniformIntArray(name, values)
}
override fun setFloatArray(name: String, values: FloatArray) = trace("setFloatArray") {
uploadFloatArray(name, values)
}
override fun setFloat(name: String, value: Float) = trace("setFloat") {
uploadUniformFloat(name, value)
}
override fun setFloat2(name: String, value: Float2) = trace("setFloat2") {
uploadUniformFloat2(name, value)
}
override fun setFloat3(name: String, value: Float3) = trace("setFloat3") {
uploadUniformFloat3(name, value)
}
override fun setFloat4(name: String, value: Float4) = trace("setFloat4") {
uploadUniformFloat4(name, value)
}
override fun setMat3(name: String, value: Mat3) = trace("setMat3") {
uploadUniformMat3(name, value)
}
override fun setMat4(name: String, value: Mat4) = trace("setMat4") {
uploadUniformMat4(name, value)
}
private fun uploadUniformInt(name: String, value: Int) {
if (intValueCache.getOrDefault(name, Int.MIN_VALUE) != value) {
val location = uniformLocation(rendererId, name)
checkGlError(GLES30.glUniform1i(location, value))
ShaderStats.shaderUploads++
intValueCache[name] = value
}
}
private fun uploadUniformIntArray(name: String, values: IntArray) {
if (!values.contentEquals(intArrayValueCache[name])) {
val location = uniformLocation(rendererId, name)
checkGlError(GLES30.glUniform1iv(location, values.size, values, 0))
ShaderStats.shaderUploads++
intArrayValueCache[name] = values
}
}
private fun uploadFloatArray(name: String, values: FloatArray) {
if (!values.contentEquals(floatArrayValueCache[name])) {
val location = uniformLocation(rendererId, name)
checkGlError(GLES30.glUniform1fv(location, values.size, values, 0))
ShaderStats.shaderUploads++
floatArrayValueCache[name] = values
}
}
private fun uploadUniformFloat(name: String, value: Float) {
if (floatValueCache.getOrDefault(name, Float.MIN_VALUE) != value) {
val location = uniformLocation(rendererId, name)
checkGlError(GLES30.glUniform1f(location, value))
ShaderStats.shaderUploads++
floatValueCache[name] = value
}
}
private fun uploadUniformFloat2(name: String, value: Float2) {
val location = uniformLocation(rendererId, name)
checkGlError(GLES30.glUniform2f(location, value.x, value.y))
ShaderStats.shaderUploads++
}
private fun uploadUniformFloat3(name: String, value: Float3) {
val location = uniformLocation(rendererId, name)
checkGlError(GLES30.glUniform3f(location, value.x, value.y, value.z))
ShaderStats.shaderUploads++
}
private fun uploadUniformFloat4(name: String, value: Float4) {
val location = uniformLocation(rendererId, name)
checkGlError(GLES30.glUniform4f(location, value.x, value.y, value.z, value.w))
ShaderStats.shaderUploads++
}
private fun uploadUniformMat3(name: String, value: Mat3) {
val location = uniformLocation(rendererId, name)
checkGlError(GLES30.glUniformMatrix3fv(location, 1, true, value.toFloatArray(), 0))
ShaderStats.shaderUploads++
}
private fun uploadUniformMat4(name: String, value: Mat4) {
val location = uniformLocation(rendererId, name)
checkGlError(GLES30.glUniformMatrix4fv(location, 1, true, value.toFloatArray(), 0))
ShaderStats.shaderUploads++
}
override fun destroy() {
unbind()
GLES30.glDetachShader(rendererId, GLES30.GL_VERTEX_SHADER)
GLES30.glDetachShader(rendererId, GLES30.GL_FRAGMENT_SHADER)
GLES30.glDeleteProgram(rendererId)
}
private fun compile(vertexSrc: String, fragmentSrc: String) = trace("shaderCompile") {
val vertexShader = GLES30.glCreateShader(GLES30.GL_VERTEX_SHADER)
checkGlError(GLES30.glShaderSource(vertexShader, vertexSrc))
checkGlError(GLES30.glCompileShader(vertexShader))
val status = IntArray(1)
GLES30.glGetShaderiv(vertexShader, GLES30.GL_COMPILE_STATUS, status, 0)
if (status[0] == GLES30.GL_FALSE) {
val errorMessage = GLES30.glGetShaderInfoLog(vertexShader)
GLES30.glDeleteShader(vertexShader)
error(errorMessage)
}
val fragmentShader = GLES30.glCreateShader(GLES30.GL_FRAGMENT_SHADER)
checkGlError(GLES30.glShaderSource(fragmentShader, fragmentSrc))
checkGlError(GLES30.glCompileShader(fragmentShader))
GLES30.glGetShaderiv(fragmentShader, GLES30.GL_COMPILE_STATUS, status, 0)
if (status[0] == GLES30.GL_FALSE) {
val errorMessage = GLES30.glGetShaderInfoLog(fragmentShader)
GLES30.glDeleteShader(fragmentShader)
Log.e(TAG, errorMessage)
error(errorMessage)
}
rendererId = GLES30.glCreateProgram()
traceName = "shaderBind[$rendererId]"
val program = rendererId
checkGlError(GLES30.glAttachShader(program, vertexShader))
checkGlError(GLES30.glAttachShader(program, fragmentShader))
GLES30.glLinkProgram(program)
GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, status, 0)
if (status[0] == GLES30.GL_FALSE) {
val errorMessage = GLES30.glGetProgramInfoLog(program)
GLES30.glDeleteProgram(program)
Log.e(TAG, errorMessage)
error(errorMessage)
}
checkGlError(GLES30.glDetachShader(program, vertexShader))
checkGlError(GLES30.glDetachShader(program, fragmentShader))
}
private fun uniformLocation(rendererId: Int, uniformName: String): Int {
return locationMap.getOrPut(uniformName) {
GLES30.glGetUniformLocation(rendererId, uniformName).also { checkGlError() }
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as OpenGLShader
return rendererId == other.rendererId
}
override fun hashCode(): Int {
return rendererId.hashCode()
}
override fun toString(): String {
return "OpenGLShader('$_name:$rendererId')"
}
private companion object {
private const val TAG = "OpenGLShader"
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/opengl/OpenGLTexture2D.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.renderer.opengl
import android.opengl.GLES11Ext
import android.opengl.GLES30
import androidx.tracing.trace
import dev.serhiiyaremych.imla.ext.checkGlError
import dev.serhiiyaremych.imla.ext.logd
import dev.serhiiyaremych.imla.renderer.Texture
import dev.serhiiyaremych.imla.renderer.Texture2D
import java.nio.Buffer
import kotlin.math.ln
internal class OpenGLTexture2D : Texture2D {
override val target: Texture.Target
override val specification: Texture.Specification
override val width: Int get() = specification.size.width
override val height: Int get() = specification.size.height
override var id: Int = 0
private set
override val flipTexture: Boolean get() = specification.flipTexture
private var _isDataLoaded: Boolean = false
constructor(target: Texture.Target, specification: Texture.Specification) : super() {
this.target = target
this.specification = specification
val glTarget = target.toGlTextureTarget()
createGLTexture(glTarget)
if (target != Texture.Target.TEXTURE_EXTERNAL_OES) {
val maxSize: Int = width.coerceAtLeast(height)
val maxLevels =
if (specification.generateMips) (1 + (ln(maxSize.toDouble()) / ln(2.0)).toInt()).coerceAtMost(
6
) else 1
logd("OpenGLTexture2D", "create texture: $target, $specification, mipmaps $maxLevels")
checkGlError(
GLES30.glTexStorage2D(
/* target = */ GLES30.GL_TEXTURE_2D,
/* levels = */ maxLevels,
/* internalformat = */ specification.format.toGlInternalFormat(),
/* width = */ width,
/* height = */ height
)
)
}
checkGlError(
GLES30.glTexParameteri(
glTarget,
GLES30.GL_TEXTURE_WRAP_S,
GLES30.GL_CLAMP_TO_EDGE
)
)
checkGlError(
GLES30.glTexParameteri(
glTarget,
GLES30.GL_TEXTURE_WRAP_T,
GLES30.GL_CLAMP_TO_EDGE
)
)
if (target != Texture.Target.TEXTURE_EXTERNAL_OES && specification.mipmapFiltering) {
checkGlError(
GLES30.glTexParameteri(
glTarget,
GLES30.GL_TEXTURE_MIN_FILTER,
GLES30.GL_LINEAR_MIPMAP_LINEAR
)
)
} else {
checkGlError(
GLES30.glTexParameteri(
glTarget,
GLES30.GL_TEXTURE_MIN_FILTER,
GLES30.GL_LINEAR
)
)
}
checkGlError(
GLES30.glTexParameteri(
glTarget,
GLES30.GL_TEXTURE_MAG_FILTER,
GLES30.GL_LINEAR
)
)
}
constructor(
textureId: Int,
target: Texture.Target,
specification: Texture.Specification
) : this(target, specification) {
this.id = textureId
_isDataLoaded = true
}
override fun generateMipMaps() = trace("glGenerateMipmap") {
if (specification.generateMips && (specification.size.width > 1 || specification.size.height > 1)) {
checkGlError(GLES30.glGenerateMipmap(/* target = */ target.toGlTextureTarget()))
}
}
private fun createGLTexture(glTarget: Int) {
val ids = IntArray(1)
checkGlError(GLES30.glGenTextures(1, ids, 0))
id = ids[0]
checkGlError(GLES30.glBindTexture(glTarget, id))
}
override fun setData(data: Buffer) {
val glTextureTarget = target.toGlTextureTarget()
checkGlError(
GLES30.glTexSubImage2D(
/* target = */ glTextureTarget,
/* level = */ 0,
/* xoffset = */ 0,
/* yoffset = */ 0,
/* width = */ width,
/* height = */ height,
/* format = */ specification.format.toGlImageFormat(),
/* type = */ specification.format.getDataType(),
/* pixels = */ data
)
)
generateMipMaps()
_isDataLoaded = true
}
override fun bind(slot: Int) = trace("textureBind") {
checkGlError(GLES30.glActiveTexture(GLES30.GL_TEXTURE0 + slot))
checkGlError(GLES30.glBindTexture(target.toGlTextureTarget(), id))
}
override fun destroy() {
GLES30.glDeleteTextures(1, intArrayOf(id), 0)
}
override fun isLoaded(): Boolean {
return _isDataLoaded
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as OpenGLTexture2D
if (target != other.target) return false
if (specification != other.specification) return false
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + target.hashCode()
result = 31 * result + specification.hashCode()
result = 31 * result + id
return result
}
override fun toString(): String {
return "OpenGLTexture2D(target=$target, specification=$specification, width=$width, height=$height, id=$id)"
}
companion object {
private const val TAG = "OpenGLTexture2D"
}
}
internal fun Texture.Target.toGlTextureTarget(): Int {
return when (this) {
Texture.Target.TEXTURE_2D -> GLES30.GL_TEXTURE_2D
Texture.Target.TEXTURE_EXTERNAL_OES -> GLES11Ext.GL_TEXTURE_EXTERNAL_OES
}
}
internal fun Texture.ImageFormat.toGlInternalFormat(): Int {
return when (this) {
Texture.ImageFormat.None -> 0
Texture.ImageFormat.A8,
Texture.ImageFormat.R8 -> GLES30.GL_R8
Texture.ImageFormat.R16F -> GLES30.GL_R16F
Texture.ImageFormat.RGB8 -> GLES30.GL_RGB8
Texture.ImageFormat.RGBA8 -> GLES30.GL_SRGB8_ALPHA8
Texture.ImageFormat.RGB10_A2 -> GLES30.GL_RGB10_A2
Texture.ImageFormat.DEPTH24STENCIL8 -> GLES30.GL_DEPTH24_STENCIL8
}
}
internal fun Texture.ImageFormat.getDataType(): Int {
// Use a when expression to return the corresponding OpenGL type constant
return when (this) {
Texture.ImageFormat.None -> 0 // No type
Texture.ImageFormat.A8 -> GLES30.GL_UNSIGNED_BYTE
Texture.ImageFormat.R8 -> GLES30.GL_UNSIGNED_BYTE
Texture.ImageFormat.R16F -> GLES30.GL_FLOAT
Texture.ImageFormat.RGB8 -> GLES30.GL_UNSIGNED_BYTE
Texture.ImageFormat.RGBA8 -> GLES30.GL_UNSIGNED_BYTE
Texture.ImageFormat.RGB10_A2 -> GLES30.GL_UNSIGNED_INT_2_10_10_10_REV
Texture.ImageFormat.DEPTH24STENCIL8 -> GLES30.GL_UNSIGNED_INT_24_8
}
}
internal fun Texture.ImageFormat.toGlImageFormat(): Int {
return when (this) {
Texture.ImageFormat.None -> 0
Texture.ImageFormat.A8 -> GLES30.GL_ALPHA
Texture.ImageFormat.R8, Texture.ImageFormat.R16F -> GLES30.GL_RED
Texture.ImageFormat.RGB8 -> GLES30.GL_RGB
Texture.ImageFormat.RGBA8, Texture.ImageFormat.RGB10_A2 -> GLES30.GL_RGBA
Texture.ImageFormat.DEPTH24STENCIL8 -> GLES30.GL_DEPTH_STENCIL
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/opengl/OpenGLUniformBuffer.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.opengl
import android.opengl.GLES30
import androidx.tracing.trace
import dev.serhiiyaremych.imla.ext.checkGlError
import dev.serhiiyaremych.imla.renderer.UniformBuffer
import dev.serhiiyaremych.imla.renderer.toFloatBuffer
import java.nio.Buffer
internal class OpenGLUniformBuffer(
count: Int,
binding: Int
) : UniformBuffer {
override val elements: Int = count
override val sizeBytes: Int
get() = elements * Float.SIZE_BYTES
private var rendererId: Int = 0
private var isBound: Boolean = false
init {
val ids = IntArray(1)
checkGlError(GLES30.glGenBuffers(1, ids, 0))
rendererId = ids[0]
bind()
checkGlError(
GLES30.glBufferData(
/* target = */ GLES30.GL_UNIFORM_BUFFER,
/* size = */ sizeBytes,
/* data = */ null,
/* usage = */ GLES30.GL_DYNAMIC_DRAW
)
)
checkGlError(
GLES30.glBindBufferBase(
/* target = */ GLES30.GL_UNIFORM_BUFFER,
/* index = */ binding,
/* buffer = */ rendererId
)
)
}
override fun setData(data: FloatArray) {
bind()
trace("uboSetData") {
checkGlError(
GLES30.glBufferSubData(
/* target = */ GLES30.GL_UNIFORM_BUFFER,
/* offset = */ 0,
/* size = */ data.size * Float.SIZE_BYTES,
/* data = */ data.toFloatBuffer()
)
)
}
}
override fun setData(data: Buffer) {
bind()
trace("uboSetData") {
checkGlError(
GLES30.glBufferSubData(
/* target = */ GLES30.GL_UNIFORM_BUFFER,
/* offset = */ 0,
/* size = */ data.capacity() * Float.SIZE_BYTES,
/* data = */ data
)
)
}
}
override fun bind() {
if (isBound.not()) {
trace("uboBind") {
checkGlError(GLES30.glBindBuffer(GLES30.GL_UNIFORM_BUFFER, rendererId))
}
isBound = true
}
}
override fun unbind() {
GLES30.glBindBuffer(GLES30.GL_UNIFORM_BUFFER, 0)
isBound = false
}
override fun destroy() {
GLES30.glDeleteBuffers(1, intArrayOf(rendererId), 0)
isBound = false
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/opengl/OpenGLVertexArray.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.opengl
import android.opengl.GLES30
import androidx.tracing.trace
import dev.serhiiyaremych.imla.ext.checkGlError
import dev.serhiiyaremych.imla.renderer.IndexBuffer
import dev.serhiiyaremych.imla.renderer.ShaderDataType
import dev.serhiiyaremych.imla.renderer.VertexArray
import dev.serhiiyaremych.imla.renderer.VertexBuffer
internal class OpenGLVertexArray : VertexArray {
private var rendererId: Int = 0
private val _vertexBuffers: MutableList = ArrayList()
val vertexBuffers: List get() = _vertexBuffers
override var indexBuffer: IndexBuffer? = null
set(value) {
field = value
checkGlError(GLES30.glBindVertexArray(rendererId))
value?.bind()
}
init {
val ids = IntArray(1)
checkGlError(GLES30.glGenVertexArrays(1, ids, 0))
rendererId = ids[0]
}
override fun bind() = trace("vaoBind") {
checkGlError(GLES30.glBindVertexArray(rendererId))
}
override fun unbind() {
GLES30.glBindVertexArray(0)
}
override fun destroy() {
GLES30.glDeleteVertexArrays(1, intArrayOf(rendererId), 0)
}
override fun addVertexBuffer(vertexBuffer: VertexBuffer) {
checkGlError(GLES30.glBindVertexArray(rendererId))
vertexBuffer.bind()
vertexBuffer.layout?.let { bufferLayout ->
bufferLayout.forEachIndexed { index, element ->
when (element.type) {
ShaderDataType.None -> { /* no-op */
}
ShaderDataType.Float,
ShaderDataType.Float2,
ShaderDataType.Float3,
ShaderDataType.Float4 -> {
checkGlError(GLES30.glEnableVertexAttribArray(index))
checkGlError(
GLES30.glVertexAttribPointer(
/* indx = */ index,
/* size = */ element.type.components,
/* type = */ element.type.openGLBaseType,
/* normalized = */ element.normalized,
/* stride = */ bufferLayout.stride,
/* offset = */ element.offset
)
)
}
ShaderDataType.Int,
ShaderDataType.Int2,
ShaderDataType.Int3,
ShaderDataType.Int4,
ShaderDataType.Bool -> {
checkGlError(GLES30.glEnableVertexAttribArray(index))
checkGlError(
GLES30.glVertexAttribIPointer(
/* index = */ index,
/* size = */ element.type.components,
/* type = */ element.type.openGLBaseType,
/* stride = */ bufferLayout.stride,
/* offset = */ element.offset
)
)
}
ShaderDataType.Mat3,
ShaderDataType.Mat4 -> {
val count = element.type.components
for (i in 0 until count) {
checkGlError(GLES30.glEnableVertexAttribArray(index))
checkGlError(
GLES30.glVertexAttribPointer(
/* indx = */ index,
/* size = */ count,
/* type = */ element.type.openGLBaseType,
/* normalized = */ element.normalized,
/* stride = */ bufferLayout.stride,
/* offset = */ (element.offset + Float.SIZE_BYTES * count * i)
)
)
checkGlError(GLES30.glVertexAttribDivisor(index, 1))
}
}
}
}
}
_vertexBuffers.add(vertexBuffer)
}
private val ShaderDataType.openGLBaseType: Int
get() = when (this) {
ShaderDataType.None -> error("Unconvertable shader data type: ${this.name}")
ShaderDataType.Float -> GLES30.GL_FLOAT
ShaderDataType.Float2 -> GLES30.GL_FLOAT
ShaderDataType.Float3 -> GLES30.GL_FLOAT
ShaderDataType.Float4 -> GLES30.GL_FLOAT
ShaderDataType.Mat3 -> GLES30.GL_FLOAT
ShaderDataType.Mat4 -> GLES30.GL_FLOAT
ShaderDataType.Int -> GLES30.GL_INT
ShaderDataType.Int2 -> GLES30.GL_INT
ShaderDataType.Int3 -> GLES30.GL_INT
ShaderDataType.Int4 -> GLES30.GL_INT
ShaderDataType.Bool -> GLES30.GL_BOOL
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/opengl/buffer/OpenGLFrameBuffer.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.opengl.buffer
import android.opengl.GLES30
import android.util.Log
import androidx.compose.ui.unit.IntSize
import androidx.tracing.trace
import dev.serhiiyaremych.imla.ext.checkGlError
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind.BOTH
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind.DRAW
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind.READ
import dev.serhiiyaremych.imla.renderer.framebuffer.Framebuffer
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferTextureFormat
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferTextureSpecification
import dev.serhiiyaremych.imla.renderer.Texture
import dev.serhiiyaremych.imla.renderer.Texture2D
import dev.serhiiyaremych.imla.renderer.opengl.toGlTextureTarget
import dev.serhiiyaremych.imla.renderer.stats.ShaderStats
import dev.serhiiyaremych.imla.renderer.toIntBuffer
internal class OpenGLFramebuffer(
spec: FramebufferSpecification
) : Framebuffer {
override var specification: FramebufferSpecification = spec
private set
override val colorAttachmentTexture: Texture2D
get() = _colorAttachmentTexture!!
override var rendererId: Int = 0
private set
private var depthAttachment: Int = 0
private val colorAttachmentSpecifications: MutableList =
mutableListOf()
private var _colorAttachmentTexture: Texture2D? = null
private var drawAttachments: IntArray = IntArray(0)
private val colorAttachments: MutableList = mutableListOf()
private var depthAttachmentSpecification: FramebufferTextureSpecification? = null
private val sampledWidth get() = specification.size.width / specification.downSampleFactor
private val sampledHeight get() = specification.size.height / specification.downSampleFactor
init {
ShaderStats.fboInstances++
invalidate()
}
override fun invalidate() {
if (rendererId != 0) {
destroy()
}
val id = IntArray(1)
GLES30.glGenFramebuffers(1, id, 0)
rendererId = id[0]
GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, rendererId)
val attachments = specification.attachmentsSpec.attachments
val colorAttachmentSpecs =
attachments.filter { it.format != FramebufferTextureFormat.DEPTH24STENCIL8 }
depthAttachmentSpecification =
attachments.find { it.format == FramebufferTextureFormat.DEPTH24STENCIL8 }
colorAttachmentSpecs.forEachIndexed { index, attachment ->
createAttachment(
width = sampledWidth,
height = sampledHeight,
format = attachment.format,
flip = attachment.flip,
mipmapFiltering = attachment.mipmapFiltering
).apply {
colorAttachments.add(this)
GLES30.glFramebufferTexture2D(
/* target = */ GLES30.GL_FRAMEBUFFER,
/* attachment = */ GLES30.GL_COLOR_ATTACHMENT0 + index,
/* textarget = */ this.target.toGlTextureTarget(),
/* texture = */ this.id,
/* level = */ 0
)
}
}
if (_colorAttachmentTexture?.id != colorAttachments.first().id) {
_colorAttachmentTexture?.destroy()
_colorAttachmentTexture = colorAttachments.first()
}
depthAttachmentSpecification?.let {
createAttachment(
width = sampledWidth,
height = sampledHeight,
format = FramebufferTextureFormat.DEPTH24STENCIL8,
flip = it.flip,
mipmapFiltering = false
).apply {
depthAttachment = this.id
GLES30.glFramebufferTexture2D(
/* target = */ GLES30.GL_FRAMEBUFFER,
/* attachment = */ GLES30.GL_DEPTH_ATTACHMENT,
/* textarget = */ GLES30.GL_TEXTURE_2D,
/* texture = */ depthAttachment,
/* level = */ 0
)
}
}
if (colorAttachmentSpecs.isNotEmpty()) {
val buffers: IntArray = IntArray(colorAttachmentSpecs.size) {
GLES30.GL_COLOR_ATTACHMENT0 + it
}
GLES30.glDrawBuffers(colorAttachmentSpecs.size, buffers, 0)
drawAttachments = buffers
} else {
// Only depth-pass
GLES30.glDrawBuffers(0, intArrayOf(), 0)
}
require(GLES30.glCheckFramebufferStatus(GLES30.GL_FRAMEBUFFER) == GLES30.GL_FRAMEBUFFER_COMPLETE) {
"OpenGL20Framebuffer is incomplete!"
}
// switchback to default framebuffer
GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0)
}
private fun readBuffer() {
GLES30.glReadBuffer(GLES30.GL_COLOR_ATTACHMENT0)
}
override fun bind(bind: Bind, updateViewport: Boolean) = trace("glBindFramebuffer[$bind]") {
GLES30.glBindFramebuffer(bind.toGlTarget(), rendererId)
if (updateViewport) {
trace("glViewport") {
GLES30.glViewport(0, 0, sampledWidth, sampledHeight)
}
}
if (bind == READ) {
readBuffer()
}
}
override fun unbind() = trace("glUnBindFramebuffer") {
GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0)
}
override fun resize(width: Int, height: Int) {
if (width == 0 || height == 0 || width > MAX_FRAMEBUFFER_SIZE || height > MAX_FRAMEBUFFER_SIZE) {
Log.w(TAG, "Attempt to resize framebuffer to $width, $height failed")
return
}
if (specification.size.width != width || specification.size.height != height) {
specification = specification.copy(
size = IntSize(width, height)
)
invalidate()
}
}
override fun invalidateAttachments() = trace("invalidateAttachments") {
checkGlError(
GLES30.glInvalidateFramebuffer(
GLES30.GL_FRAMEBUFFER,
drawAttachments.size,
drawAttachments,
0
)
)
}
override fun getColorAttachmentRendererID(index: Int): Int {
require(index <= colorAttachments.lastIndex)
return colorAttachments[index].id
}
override fun clearAttachment(attachmentIndex: Int, value: Int) {
val attachmentHandle = getColorAttachmentRendererID(attachmentIndex)
val spec = colorAttachmentSpecifications[attachmentIndex]
val textureFormat = spec.format
val type = when (textureFormat) {
FramebufferTextureFormat.RGBA8, FramebufferTextureFormat.R8 -> GLES30.GL_UNSIGNED_BYTE
FramebufferTextureFormat.RGB10_A2 -> GLES30.GL_UNSIGNED_INT_2_10_10_10_REV
FramebufferTextureFormat.DEPTH24STENCIL8 -> GLES30.GL_UNSIGNED_INT_24_8
}
val components = when (textureFormat) {
FramebufferTextureFormat.R8 -> 1
FramebufferTextureFormat.RGBA8 -> 4
FramebufferTextureFormat.RGB10_A2 -> 4
FramebufferTextureFormat.DEPTH24STENCIL8 -> 1
}
val emptyPixels = IntArray(
size = sampledWidth * sampledHeight * components,
init = { value }
).toIntBuffer()
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, attachmentHandle)
GLES30.glTexSubImage2D(
/* target = */ GLES30.GL_TEXTURE_2D,
/* level = */ 0,
/* xoffset = */ 0,
/* yoffset = */ 0,
/* width = */ sampledWidth,
/* height = */ sampledHeight,
/* format = */ fbTextureFormatToGL(textureFormat),
/* type = */ type,
/* pixels = */ emptyPixels
)
}
override fun setColorAttachmentAt(attachmentIndex: Int) {
require(attachmentIndex <= colorAttachments.lastIndex)
_colorAttachmentTexture = colorAttachments[attachmentIndex]
}
override fun destroy() {
unbind()
GLES30.glDeleteFramebuffers(1, intArrayOf(rendererId), 0)
GLES30.glDeleteTextures(1, intArrayOf(depthAttachment), 0)
GLES30.glDeleteTextures(
colorAttachments.size,
colorAttachments.map { it.id }.toIntArray(),
0
)
}
override fun toString(): String {
return "OpenGLFramebuffer(rendererId=$rendererId, specification=$specification, drawAttachments=${drawAttachments.contentToString()}, sampledWidth=$sampledWidth, sampledHeight=$sampledHeight)"
}
private companion object {
private const val TAG = "OpenGLFramebuffer"
const val MAX_FRAMEBUFFER_SIZE = 8192
fun fbTextureFormatToGL(format: FramebufferTextureFormat): Int {
return when (format) {
FramebufferTextureFormat.R8 -> GLES30.GL_R8
FramebufferTextureFormat.RGBA8 -> GLES30.GL_RGBA8
FramebufferTextureFormat.RGB10_A2 -> GLES30.GL_RGB10_A2
FramebufferTextureFormat.DEPTH24STENCIL8 -> GLES30.GL_DEPTH24_STENCIL8
}
}
fun createAttachment(
width: Int,
height: Int,
format: FramebufferTextureFormat,
flip: Boolean,
mipmapFiltering: Boolean
): Texture2D = Texture2D.create(
target = Texture.Target.TEXTURE_2D,
specification = Texture.Specification(
size = IntSize(width = width, height = height),
format = format.toTextureFormat(),
flipTexture = flip,
generateMips = mipmapFiltering,
mipmapFiltering = mipmapFiltering
)
)
}
}
internal fun Bind.toGlTarget(): Int {
return when (this) {
READ -> GLES30.GL_READ_FRAMEBUFFER
DRAW -> GLES30.GL_DRAW_FRAMEBUFFER
BOTH -> GLES30.GL_FRAMEBUFFER
}
}
private fun FramebufferTextureFormat.toTextureFormat(): Texture.ImageFormat {
return when (this) {
FramebufferTextureFormat.R8 -> Texture.ImageFormat.R8
FramebufferTextureFormat.RGBA8 -> Texture.ImageFormat.RGBA8
FramebufferTextureFormat.RGB10_A2 -> Texture.ImageFormat.RGB10_A2
FramebufferTextureFormat.DEPTH24STENCIL8 -> Texture.ImageFormat.DEPTH24STENCIL8
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/opengl/buffer/OpenGLIndexBuffer.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.opengl.buffer
import android.opengl.GLES30
import dev.serhiiyaremych.imla.ext.checkGlError
import dev.serhiiyaremych.imla.renderer.IndexBuffer
import dev.serhiiyaremych.imla.renderer.toIntBuffer
internal class OpenGLIndexBuffer(indices: IntArray) : IndexBuffer {
override val elements: Int = indices.size
override val sizeBytes: Int = elements * Int.SIZE_BYTES
private var rendererId: Int = 0
private var isDestroyed: Boolean = false
init {
val ids = IntArray(1)
checkGlError(GLES30.glGenBuffers(1, ids, 0))
rendererId = ids[0]
checkGlError(GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, rendererId))
checkGlError(
GLES30.glBufferData(
/* target = */ GLES30.GL_ELEMENT_ARRAY_BUFFER,
/* size = */ sizeBytes,
/* data = */ indices.toIntBuffer(),
/* usage = */ GLES30.GL_STATIC_DRAW
)
)
}
override fun bind() {
if (isDestroyed) {
error("Can't bind destroyed index buffer.")
}
checkGlError(GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, rendererId))
}
override fun unbind() {
if (!isDestroyed) {
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0)
}
}
override fun destroy() {
unbind()
GLES30.glDeleteBuffers(1, intArrayOf(rendererId), 0)
isDestroyed = true
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/opengl/buffer/OpenGLVertexBuffer.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.opengl.buffer
import android.opengl.GLES30
import androidx.tracing.trace
import dev.serhiiyaremych.imla.ext.checkGlError
import dev.serhiiyaremych.imla.renderer.BufferLayout
import dev.serhiiyaremych.imla.renderer.VertexBuffer
import dev.serhiiyaremych.imla.renderer.toFloatBuffer
internal class OpenGLVertexBuffer : VertexBuffer {
override var elements: Int
override var sizeBytes: Int
override var layout: BufferLayout? = null
private var bufferId: Int = 0
private var isDestroyed: Boolean = false
constructor(count: Int) {
this.elements = count
this.sizeBytes = this.elements * Float.SIZE_BYTES
trace("vboCreateDynamic") {
createVertexBuffer(null, GLES30.GL_DYNAMIC_DRAW)
}
}
constructor(vertices: FloatArray) {
this.elements = vertices.size
this.sizeBytes = elements * Float.SIZE_BYTES
trace("vboCreateStatic") {
createVertexBuffer(vertices, GLES30.GL_STATIC_DRAW)
}
}
private fun createVertexBuffer(vertices: FloatArray?, usage: Int) {
val ids = IntArray(1)
checkGlError(GLES30.glGenBuffers(1, ids, 0))
bufferId = ids[0]
checkGlError(GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, bufferId))
checkGlError(
GLES30.glBufferData(
/* target = */ GLES30.GL_ARRAY_BUFFER,
/* size = */ sizeBytes,
/* data = */ vertices?.toFloatBuffer(),
/* usage = */ usage
)
)
}
override fun bind() = trace("vboBind") {
if (isDestroyed) {
error("Can't bind destroyed buffer.")
}
checkGlError(GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, bufferId))
}
override fun unbind() {
if (!isDestroyed) {
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0)
}
}
override fun setData(data: FloatArray) = trace("vboSetData") {
bind()
this.sizeBytes = data.size * Float.SIZE_BYTES
this.elements = data.size
trace("glBufferSubData[${elements}, ${sizeBytes}bytes]") {
checkGlError(
GLES30.glBufferSubData(GLES30.GL_ARRAY_BUFFER, 0, sizeBytes, data.toFloatBuffer())
)
}
}
override fun destroy() {
unbind()
GLES30.glDeleteBuffers(1, intArrayOf(bufferId), 0)
isDestroyed = true
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/primitive/QuadVertex.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.primitive
import androidx.compose.ui.geometry.Offset
import dev.romainguy.kotlin.math.Float4
internal data class QuadVertex(
val position: Float4,
val texCoord: Offset,
val texIndex: Float,
val flipTexture: Float,
val isExternalTexture: Float,
val alpha: Float,
val mask: Float,
val maskCoord: Offset
) {
companion object {
// @formatter:off
const val NUMBER_OF_COMPONENTS =
/*position*/ 4 +
/*texCoord*/ 2 +
/* texIndex */ 1 +
/* flipTexture */ 1 +
/* isExternalTexture */ 1 +
/* alpha */ 1 +
/* mask */ 1 +
/* maskCoord */ 2
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/shader/Shader.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.renderer.shader
import android.content.res.AssetManager
import dev.romainguy.kotlin.math.Float2
import dev.romainguy.kotlin.math.Float3
import dev.romainguy.kotlin.math.Float4
import dev.romainguy.kotlin.math.Mat3
import dev.romainguy.kotlin.math.Mat4
import dev.serhiiyaremych.imla.renderer.opengl.OpenGLShader
import org.intellij.lang.annotations.Language
import java.io.InputStream
internal interface Shader {
val name: String
@Deprecated("")
fun bind()
fun bind(shaderBinder: ShaderBinder)
fun unbind()
fun bindUniformBlock(blockName: String, bindingPoint: Int)
fun setInt(name: String, value: Int)
fun setIntArray(name: String, values: IntArray)
fun setFloatArray(name: String, values: FloatArray)
fun setFloat(name: String, value: Float)
fun setFloat2(name: String, value: Float2)
fun setFloat3(name: String, value: Float3)
fun setFloat4(name: String, value: Float4)
fun setMat3(name: String, value: Mat3)
fun setMat4(name: String, value: Mat4)
fun destroy()
companion object {
private const val TAG = "Shader"
private fun dropExtension(fileName: String): String {
val lastIndexOfDot = fileName.lastIndexOf(".")
return if (lastIndexOfDot != -1) fileName.substring(0, lastIndexOfDot) else fileName
}
private fun readWithCloseStream(inputStream: InputStream): String {
return inputStream.bufferedReader().readText().also { inputStream.close() }
}
fun create(assetManager: AssetManager, vertexAsset: String, fragmentAsset: String): Shader {
return OpenGLShader(
name = dropExtension(vertexAsset),
vertexSrc = readWithCloseStream(assetManager.open(vertexAsset)),
fragmentSrc = readWithCloseStream(assetManager.open(fragmentAsset))
)
}
fun create(
name: String,
@Language("GLSL") vertexSrc: String,
@Language("GLSL") fragmentSrc: String
): Shader {
return OpenGLShader(name, vertexSrc, fragmentSrc)
}
fun create(
assetManager: AssetManager,
@Language("GLSL") fragmentSrc: String
): Shader {
return OpenGLShader(
name = "simple_quad",
vertexSrc = readWithCloseStream(assetManager.open("shader/simple_quad.vert")),
fragmentSrc = fragmentSrc
)
}
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/shader/ShaderBinder.kt
================================================
package dev.serhiiyaremych.imla.renderer.shader
internal class ShaderBinder {
private var currentShader: Shader? = null
internal fun bind(shader: Shader) {
if (shader != currentShader) {
currentShader?.unbind()
shader.bind()
currentShader = shader
}
}
internal fun destroyCurrent() {
currentShader?.destroy()
currentShader = null
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/shader/ShaderLibrary.kt
================================================
package dev.serhiiyaremych.imla.renderer.shader
import android.content.res.AssetManager
import androidx.tracing.trace
import org.intellij.lang.annotations.Language
internal class ShaderLibrary(private val assetManager: AssetManager) {
private val shaders: MutableMap = mutableMapOf()
fun loadShaderFromFile(vertFileName: String, fragFileName: String): Shader =
trace("ShaderLibrary#loadShader") {
return shaders.getOrPut("${vertFileName}_$fragFileName") {
Shader.create(
assetManager,
"shader/$vertFileName.vert",
"shader/$fragFileName.frag"
)
}
}
fun loadShader(
name: String,
@Language("GLSL") vertexSrc: String,
@Language("GLSL") fragmentSrc: String
): Shader {
return shaders.getOrPut(name) {
Shader.create(name, vertexSrc, fragmentSrc)
}
}
fun loadShader(
name: String,
@Language("GLSL") fragmentSrc: String
): Shader {
return shaders.getOrPut(name) {
Shader.create(assetManager, fragmentSrc)
}
}
fun destroy(shader: Shader) {
shader.destroy()
shaders.remove(shader.name)
}
fun destroyAll() {
shaders.forEach { (_, shader) -> shader.destroy() }
shaders.clear()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/shader/ShaderProgram.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.shader
import dev.serhiiyaremych.imla.renderer.BufferLayout
import dev.serhiiyaremych.imla.renderer.primitive.QuadVertex
internal interface ShaderProgram {
val shader: Shader
val vertexBufferLayout: BufferLayout
val componentsCount: Int
fun mapVertexData(quadVertexBufferBase: List): FloatArray
fun destroy()
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/stats/ShaderStats.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.stats
import android.util.Log
internal object ShaderStats {
private const val TAG = "ShaderStats"
var fboInstances: Int = 0
var shaderInstances: Int = 0
var shaderBinds: Int = 0
var shaderBindUniformBlock: Int = 0
var shaderUploads: Int = 0
fun printStats() {
Log.d(TAG, "--------ShaderStats--------")
Log.d(
TAG, """
fboInstances = $fboInstances
shaderInstances = $shaderInstances
shaderBinds = $shaderBinds
shaderUploads = $shaderUploads
shaderBindUniformBlock = $shaderBindUniformBlock
""".trimIndent()
)
Log.d(TAG, "---------------------------")
}
fun reset() {
shaderBinds = 0
shaderUploads = 0
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/renderer/util/SizeUtil.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.renderer.util
import androidx.compose.ui.unit.IntSize
internal object SizeUtil {
private val powersOfTwo = intArrayOf(
2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096
)
fun closestPOTDown(arbitrarySize: Int): Int {
if (arbitrarySize <= 2) return 2
return powersOfTwo.last { it <= arbitrarySize }
}
fun closestPOTUp(arbitrarySize: Int): Int {
if (arbitrarySize <= 2) return 2
return powersOfTwo.first { it >= arbitrarySize }
}
fun closestPOTDown(arbitrarySize: IntSize): IntSize {
return IntSize(
width = closestPOTDown(arbitrarySize.width),
height = closestPOTDown(arbitrarySize.height)
)
}
fun closestPOTUp(arbitrarySize: IntSize): IntSize {
return IntSize(
width = closestPOTUp(arbitrarySize.width),
height = closestPOTUp(arbitrarySize.height)
)
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/ui/BackdropBlur.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.ui
import android.view.Surface
import androidx.compose.foundation.AndroidExternalSurface
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.addOutline
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toIntSize
import androidx.compose.ui.util.trace
import dev.serhiiyaremych.imla.uirenderer.Style
import dev.serhiiyaremych.imla.uirenderer.UiLayerRenderer
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import java.util.UUID
@Composable
public fun BackdropBlur(
modifier: Modifier,
rendererState: UiLayerRenderer,
style: Style = Style.default,
blurMask: Brush? = null,
clipShape: Shape = RectangleShape,
content: @Composable (onOffsetChanged: (Offset) -> Unit) -> Unit = {}
): Unit = Box {
val contentBoundingBoxState = remember { mutableStateOf(Rect.Zero) }
val id = remember { trace("BlurBehindView#id") { UUID.randomUUID().toString() } }
val drawingSurfaceState = remember { mutableStateOf(null) }
val drawingSurfaceSizeState = remember { mutableStateOf(IntSize.Zero) }
val contentOffset = remember { mutableStateOf(Offset.Zero) }
val contentBoundingBox = contentBoundingBoxState.value
val clipPath = remember { Path() }
// Render the external surface
AndroidExternalSurface(
modifier = modifier
.onGloballyPositioned { layoutCoordinates ->
contentBoundingBoxState.value = layoutCoordinates.boundsInRoot()
}
.drawWithCache {
val outline = clipShape.createOutline(size, layoutDirection, this)
clipPath.rewind()
clipPath.addOutline(outline)
onDrawWithContent {
clipPath(path = clipPath) {
this@onDrawWithContent.drawContent()
}
}
},
surfaceSize = contentBoundingBox.size.toIntSize(),
) {
onSurface { surface, w, h ->
Snapshot.withMutableSnapshot {
drawingSurfaceState.value = surface
drawingSurfaceSizeState.value = IntSize(w, h)
}
surface.onChanged { _, _ ->
// todo
}
surface.onDestroyed {
rendererState.detachRenderObject(id)
drawingSurfaceState.value = null
}
}
}
val isRendererInitialized by rendererState.isInitialized
val topOffset = Offset(
x = contentBoundingBox.left,
y = contentBoundingBox.top
)
LaunchedEffect(id, drawingSurfaceState.value, rendererState, contentBoundingBox) {
val rendererFlow = snapshotFlow { isRendererInitialized }
val surfaceFlow = snapshotFlow { drawingSurfaceState.value }
combine(rendererFlow, surfaceFlow) { a, b -> a to b }
.filter { it.first && it.second != null }
.distinctUntilChanged()
.collect {
rendererState.attachRendererSurface(
surface = it.second,
id = id,
size = drawingSurfaceSizeState.value,
)
rendererState.updateOffset(id, topOffset + contentOffset.value)
rendererState.updateStyle(id, style)
rendererState.updateMask(id, blurMask)
}
}
rendererState.updateMask(id, blurMask)
rendererState.updateOffset(id, topOffset + contentOffset.value)
trace("BackdropBlurView#renderObject.style") {
rendererState.updateStyle(id, style)
}
// Render the content and handle offset changes
content { offset ->
contentOffset.value = offset
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/MaskTextureRenderer.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer
import android.graphics.PorterDuff
import android.graphics.SurfaceTexture
import android.view.Surface
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.util.trace
import androidx.graphics.opengl.GLRenderer
import dev.serhiiyaremych.imla.ext.isGLThread
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
import dev.serhiiyaremych.imla.renderer.framebuffer.Framebuffer
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferAttachmentSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferTextureFormat
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferTextureSpecification
import dev.serhiiyaremych.imla.renderer.RenderCommand
import dev.serhiiyaremych.imla.renderer.shader.Shader
import dev.serhiiyaremych.imla.renderer.SimpleRenderer
import dev.serhiiyaremych.imla.renderer.Texture
import dev.serhiiyaremych.imla.renderer.Texture2D
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import dev.serhiiyaremych.imla.uirenderer.processing.SimpleQuadRenderer
import java.util.concurrent.atomic.AtomicBoolean
// TODO: Refactor it to custom shader
internal class MaskTextureRenderer(
density: Density,
private val shaderLibrary: ShaderLibrary,
private val shaderBinder: ShaderBinder,
private val simpleQuadRenderer: SimpleQuadRenderer,
private val onRenderComplete: (Texture2D) -> Unit
) : Density by density {
private val drawScope = CanvasDrawScope()
private lateinit var frameBuffer: Framebuffer
private lateinit var maskExternalTexture: Texture2D
private lateinit var surfaceTexture: SurfaceTexture
private lateinit var extOesShaderProgram: Shader
private lateinit var surface: Surface
private var isInitialized: AtomicBoolean = AtomicBoolean(false)
private var lastRenderedBrush: Brush? = null
private fun initialize(size: IntSize) {
require(isGLThread()) { "Initialization failed: An active GL context is required in the current thread." }
if (isInitialized.get()) {
destroy()
}
extOesShaderProgram = shaderLibrary
.loadShaderFromFile(vertFileName = "simple_quad", fragFileName = "simple_ext_quad")
.apply {
bind(shaderBinder)
bindUniformBlock(
SimpleRenderer.TEXTURE_DATA_UBO_BLOCK,
SimpleRenderer.TEXTURE_DATA_UBO_BINDING_POINT
)
}
frameBuffer = Framebuffer.create(
FramebufferSpecification(
size = size,
attachmentsSpec = FramebufferAttachmentSpecification(
attachments = listOf(FramebufferTextureSpecification(format = FramebufferTextureFormat.R8))
)
)
)
val texSpec = Texture.Specification(
size = size,
format = Texture.ImageFormat.RGBA8,
flipTexture = true
)
maskExternalTexture =
Texture2D.create(target = Texture.Target.TEXTURE_EXTERNAL_OES, specification = texSpec)
maskExternalTexture.bind()
surfaceTexture = SurfaceTexture(maskExternalTexture.id)
surfaceTexture.setDefaultBufferSize(size.width, size.height)
surfaceTexture.setOnFrameAvailableListener {
it.updateTexImage()
copyTextureToFrameBuffer()
onRenderComplete(frameBuffer.colorAttachmentTexture)
}
surface = Surface(surfaceTexture)
isInitialized.set(true)
}
private fun copyTextureToFrameBuffer() = trace(
sectionName = "copyExtTextureToFrameBuffer"
) {
frameBuffer.bind(Bind.DRAW)
RenderCommand.clear(Color.Transparent)
simpleQuadRenderer.draw(shader = extOesShaderProgram, texture = maskExternalTexture)
}
private fun invalidateBySize(newSize: IntSize): Boolean {
return !isInitialized.get() ||
(maskExternalTexture.width != newSize.width || maskExternalTexture.height != newSize.height)
}
private fun shouldRedraw(brush: Brush): Boolean {
return lastRenderedBrush != brush
}
fun renderMask(glRenderer: GLRenderer, brush: Brush, size: IntSize) =
trace("MaskTextureRenderer#renderMask") {
if (invalidateBySize(size)) glRenderer.execute { this.initialize(size) }
if (shouldRedraw(brush)) {
glRenderer.execute {
trace("MaskTextureRenderer#shouldRedraw") {
val hwCanvas = surface.lockHardwareCanvas()
hwCanvas.drawColor(Color.Transparent.toArgb(), PorterDuff.Mode.CLEAR)
drawScope.draw(
density = this,
layoutDirection = LayoutDirection.Ltr,
canvas = Canvas(hwCanvas),
size = size.toSize()
) {
drawRect(brush)
}
surface.unlockCanvasAndPost(hwCanvas)
}
}
} else {
this.onRenderComplete(maskExternalTexture)
}
}
fun destroy() {
if (isInitialized.get()) {
surfaceTexture.release()
surface.release()
maskExternalTexture.destroy()
isInitialized.set(false)
frameBuffer.destroy()
}
}
fun releaseCurrentMask() {
destroy()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/RenderObject.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused", "CanBeParameter")
package dev.serhiiyaremych.imla.uirenderer
import android.graphics.PixelFormat
import android.hardware.HardwareBuffer
import android.media.ImageReader
import android.os.Build
import android.view.Surface
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.toIntSize
import androidx.compose.ui.util.trace
import androidx.graphics.opengl.GLRenderer
import androidx.graphics.opengl.egl.EGLManager
import dev.serhiiyaremych.imla.renderer.Texture2D
import kotlin.properties.Delegates
internal class RenderObject internal constructor(
internal val id: String,
internal var area: Rect,
) {
private var renderCallback: ((RenderObject) -> Unit)? = null
private val openGLCallback = object : GLRenderer.RenderCallback {
override fun onDrawFrame(eglManager: EGLManager) {
trace("RenderObject#onRender") {
renderCallback?.invoke(this@RenderObject)
}
}
}
internal var style: Style by Delegates.observable(Style.default) { _, old, new ->
if (old != new) {
invalidate()
}
}
internal var mask: Texture2D? = null
var renderTarget: GLRenderer.RenderTarget? = null
fun invalidate(onRenderComplete: ((GLRenderer.RenderTarget) -> Unit)? = null) {
renderTarget?.requestRender(onRenderComplete)
}
fun setRenderCallback(onRender: ((RenderObject) -> Unit)?) {
this.renderCallback = onRender
}
fun updateOffset(offset: Offset) = trace("RenderObject#updateOffset") {
area = area.translate(translateX = offset.x, translateY = offset.y)
invalidate()
}
override fun toString(): String {
return "RenderObject(id='$id', rect='$area'')"
}
fun detachFromRenderer() {
renderTarget?.detach(true)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as RenderObject
if (id != other.id) return false
if (area != other.area) return false
if (style != other.style) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + area.hashCode()
result = 31 * result + style.hashCode()
return result
}
companion object {
private fun createImageReader(width: Int, height: Int): ImageReader {
val maxImages = 2
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ImageReader.newInstance(
/* width = */ width,
/* height = */ height,
/* format = */ PixelFormat.RGBA_8888,
/* maxImages = */ maxImages,
/* usage = */
HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE or HardwareBuffer.USAGE_GPU_COLOR_OUTPUT
)
} else {
ImageReader.newInstance(
/* width = */ width,
/* height = */ height,
/* format = */ PixelFormat.RGBA_8888,
/* maxImages = */ maxImages
)
}
}
fun createFromSurface(
id: String,
renderableLayer: RenderableRootLayer,
glRenderer: GLRenderer,
surface: Surface,
rect: Rect,
): RenderObject {
val renderObject = RenderObject(
id = id,
area = rect,
).apply {
renderTarget = glRenderer.attach(
surface = surface,
width = rect.width.toInt(),
height = rect.height.toInt(),
renderer = openGLCallback
)
}
return renderObject
}
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/RenderableRootLayer.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.SurfaceTexture
import android.view.Surface
import androidx.annotation.MainThread
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
import androidx.tracing.trace
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
import dev.serhiiyaremych.imla.renderer.framebuffer.Framebuffer
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferAttachmentSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferTextureFormat
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferTextureSpecification
import dev.serhiiyaremych.imla.renderer.RenderCommand
import dev.serhiiyaremych.imla.renderer.Renderer2D
import dev.serhiiyaremych.imla.renderer.shader.Shader
import dev.serhiiyaremych.imla.renderer.SimpleRenderer
import dev.serhiiyaremych.imla.renderer.Texture
import dev.serhiiyaremych.imla.renderer.Texture2D
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import dev.serhiiyaremych.imla.uirenderer.processing.SimpleQuadRenderer
internal class RenderableRootLayer(
private val shaderLibrary: ShaderLibrary,
private val shaderBinder: ShaderBinder,
private val layerDownsampleFactor: Int,
private val density: Density,
internal val graphicsLayer: GraphicsLayer,
internal val renderer2D: Renderer2D,
private val simpleQuadRenderer: SimpleQuadRenderer,
private val onLayerTextureUpdated: () -> Unit
) {
val sizeInt: IntSize get() = graphicsLayer.size
val sizeDec: Size get() = sizeInt.toSize()
val scale: Float
get() = 1.0f / layerDownsampleFactor
val isReady: Boolean
get() = sizeInt == IntSize.Zero
private val drawingScope: CanvasDrawScope = CanvasDrawScope()
private lateinit var layerExternalTexture: SurfaceTexture
private lateinit var layerSurface: Surface
private lateinit var extOesLayerTexture: Texture2D
lateinit var highResFBO: Framebuffer
private set
private var isInitialized: Boolean = false
private var isDestroyed: Boolean = false
private lateinit var extOesShaderProgram: Shader
fun initialize() {
require(!isDestroyed) { "Can't re-init destroyed layer" }
if (!isReady) {
trace("RenderableRootLayer#initialize") {
val specification = FramebufferSpecification(
size = sizeInt,
attachmentsSpec = FramebufferAttachmentSpecification(
listOf(
FramebufferTextureSpecification(
format = FramebufferTextureFormat.RGBA8,
flip = true
)
)
),
)
highResFBO = Framebuffer.create(specification)
extOesLayerTexture = Texture2D.create(
target = Texture.Target.TEXTURE_EXTERNAL_OES,
specification = Texture.Specification(size = sizeInt, flipTexture = false)
)
extOesLayerTexture.bind()
layerExternalTexture = SurfaceTexture(extOesLayerTexture.id)
layerExternalTexture.setDefaultBufferSize(sizeInt.width, sizeInt.height)
layerSurface = Surface(layerExternalTexture)
layerExternalTexture.setOnFrameAvailableListener {
trace("surfaceTexture#updateTexImage") { it.updateTexImage() }
copyTextureToFrameBuffer()
onLayerTextureUpdated()
}
extOesShaderProgram = shaderLibrary.loadShaderFromFile(
vertFileName = "simple_quad",
fragFileName = "simple_ext_quad"
).apply {
bind(shaderBinder)
bindUniformBlock(
SimpleRenderer.TEXTURE_DATA_UBO_BLOCK,
SimpleRenderer.TEXTURE_DATA_UBO_BINDING_POINT
)
}
isInitialized = true
}
}
}
fun resize() {
TODO("Implement runtime layer resizing")
}
private fun copyTextureToFrameBuffer() = trace(
"copyExtTextureToFrameBuffer"
) {
trace("fullSizeBuffer") {
highResFBO.bind(Bind.DRAW)
RenderCommand.clear()
simpleQuadRenderer.draw(extOesShaderProgram, extOesLayerTexture)
}
}
@MainThread
fun updateTex() = trace("RenderableRootLayer#updateTex") {
require(!isDestroyed) { "Can't update destroyed layer" }
require(!graphicsLayer.isReleased) { "GraphicsLayer has been released!" }
require(isInitialized) { "RenderableRootLayer not initialized!" }
trace("drawLayerToExtTexture[$sizeInt]") {
layerSurface.lockHardwareCanvas()?.let { canvas ->
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
drawingScope.draw(density, LayoutDirection.Ltr, Canvas(canvas), sizeDec) {
trace("drawGraphicsLayer") {
scale(scaleX = 1.0f, scaleY = -1f) {
drawLayer(graphicsLayer)
}
}
}
layerSurface.unlockCanvasAndPost(canvas)
}
}
}
fun destroy() {
layerExternalTexture.release()
layerSurface.release()
extOesLayerTexture.destroy()
isDestroyed = true
}
}
internal operator fun IntSize.compareTo(other: IntSize): Int {
return (width.toLong() * height).compareTo((other.width.toLong() * other.height))
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/RenderingPipeline.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.uirenderer
import android.view.View
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.trace
import androidx.graphics.opengl.GLRenderer
import androidx.tracing.Trace
import dev.serhiiyaremych.imla.renderer.RenderCommand
import dev.serhiiyaremych.imla.renderer.Renderer2D
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferPool
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import dev.serhiiyaremych.imla.renderer.stats.ShaderStats
import dev.serhiiyaremych.imla.uirenderer.processing.EffectCoordinator
import dev.serhiiyaremych.imla.uirenderer.processing.SimpleQuadRenderer
import java.util.concurrent.ConcurrentHashMap
internal class RenderingPipeline(
rootLayer: RenderableRootLayer,
private val simpleRenderer: SimpleQuadRenderer,
private val shaderLibrary: ShaderLibrary,
private val shaderBinder: ShaderBinder,
private val renderer2D: Renderer2D,
private val density: Density
) {
private val framebufferPool: FramebufferPool = FramebufferPool()
private val masks: MutableMap = ConcurrentHashMap()
private val renderObjects: MutableMap = ConcurrentHashMap()
private val effectCoordinator = EffectCoordinator(
density = density,
framebufferPool = framebufferPool,
rootLayer = rootLayer,
simpleQuadRenderer = simpleRenderer,
shaderLibrary = shaderLibrary,
shaderBinder = shaderBinder
)
fun getRenderObject(id: String?): RenderObject? {
return id?.let { renderObjects[it] }
}
fun addRenderObject(renderObject: RenderObject) {
renderObjects[renderObject.id] = renderObject.apply { setRenderCallback(renderCallback) }
}
fun updateMask(glRenderer: GLRenderer, renderObjectId: String?, mask: Brush?) {
val renderObject = renderObjectId?.let { renderObjects[it] }
if (renderObject != null) {
val maskRenderer = masks.getOrPut(renderObject.id) {
MaskTextureRenderer(
density = density,
shaderLibrary = shaderLibrary,
shaderBinder = shaderBinder,
simpleQuadRenderer = simpleRenderer,
onRenderComplete = { tex ->
renderObject.mask = tex
renderObject.invalidate()
}
)
}
if (mask != null) {
maskRenderer.renderMask(
glRenderer = glRenderer,
brush = mask,
size = IntSize(
width = renderObject.area.size.width.toInt(),
height = renderObject.area.size.height.toInt()
)
)
} else {
maskRenderer.releaseCurrentMask()
renderObject.mask = null
renderObject.invalidate()
}
}
}
private val renderCallback = fun(renderObject: RenderObject) {
trace("RenderingPipeline#applyAllEffects") {
effectCoordinator.applyEffects(renderObject)
framebufferPool.resetPool()
framebufferPool.eraseAll()
}
}
fun requestRender(onRenderComplete: () -> Unit) {
val renderObjectsCount = renderObjects.size
if (renderObjectsCount == 0) {
onRenderComplete()
return
}
var remainingRenders = renderObjectsCount
val id = View.generateViewId()
Trace.beginAsyncSection("requestRender", id)
renderObjects.forEach { (_, renderObject) ->
renderObject.invalidate {
if (--remainingRenders == 0) {
onRenderComplete()
RenderCommand.bindDefaultFramebuffer()
RenderCommand.useDefaultProgram()
RenderCommand.clear()
Trace.endAsyncSection("requestRender", id)
ShaderStats.printStats()
ShaderStats.reset()
}
}
}
}
fun removeRenderObject(renderObjectId: String?) {
val renderObject = renderObjects.remove(renderObjectId)
renderObject?.setRenderCallback(null)
effectCoordinator.removeEffectsOf(renderObjectId)
}
fun destroy() {
renderObjects.forEach { (_, ro) ->
ro.detachFromRenderer()
ro.setRenderCallback(null)
effectCoordinator.removeEffectsOf(ro.id)
}
masks.forEach { (_, mask) ->
mask.destroy()
}
renderObjects.clear()
masks.clear()
effectCoordinator.destroy()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/Style.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import dev.serhiiyaremych.imla.uirenderer.processing.blur.BlurContext
@Immutable
public data class Style(
@FloatRange(from = 0.1, to = 2.0)
val offset: Float,
@IntRange(from = 0, to = BlurContext.MAX_PASSES.toLong())
val passes: Int,
val tint: Color = Color.Cyan.copy(alpha = 0.3f),
@FloatRange(from = 0.0, to = 1.0) val noiseAlpha: Float = 0.07f,
@FloatRange(from = 0.0, to = 1.0) val blurOpacity: Float = 1.0f,
) {
public companion object {
public val default: Style = Style(
offset = 1.5f,
passes = 4,
tint = Color.Transparent,
noiseAlpha = 0.2f
)
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/UiLayerRenderer.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package dev.serhiiyaremych.imla.uirenderer
import android.content.res.AssetManager
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Surface
import androidx.annotation.MainThread
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.util.trace
import androidx.graphics.opengl.GLRenderer
import androidx.graphics.opengl.egl.EGLManager
import dev.serhiiyaremych.imla.BuildConfig
import dev.serhiiyaremych.imla.ext.logw
import dev.serhiiyaremych.imla.renderer.RenderCommand
import dev.serhiiyaremych.imla.renderer.Renderer2D
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.SimpleRenderer
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import dev.serhiiyaremych.imla.uirenderer.processing.SimpleQuadRenderer
import java.util.concurrent.atomic.AtomicBoolean
internal class UiRendererObserver(
val uiLayerRenderer: UiLayerRenderer
) : RememberObserver {
override fun onAbandoned() {
uiLayerRenderer.destroy()
}
override fun onForgotten() {
uiLayerRenderer.destroy()
}
override fun onRemembered() {
// no-op
logw("UiLayerRenderer", "UiLayerRenderer $uiLayerRenderer has been instantiated")
}
}
@Composable
public fun rememberUiLayerRenderer(downSampleFactor: Int = 2): UiLayerRenderer {
val density = LocalDensity.current
val graphicsLayer = rememberGraphicsLayer()
val assetManager = LocalContext.current.assets
return remember(density, graphicsLayer, assetManager, downSampleFactor) {
UiRendererObserver(
UiLayerRenderer(
density,
graphicsLayer,
downSampleFactor,
assetManager
)
).uiLayerRenderer
}
}
@Stable
public class UiLayerRenderer(
density: Density,
graphicsLayer: GraphicsLayer,
downSampleFactor: Int,
assetManager: AssetManager
) : Density by density {
// Consider moving to RendererContext class
private val shaderLibrary: ShaderLibrary = ShaderLibrary(assetManager)
private val renderer2D: Renderer2D = Renderer2D()
private val shaderBinder = ShaderBinder()
private val simpleRenderer: SimpleRenderer = SimpleRenderer()
private val simpleQuadRenderer: SimpleQuadRenderer =
SimpleQuadRenderer(shaderLibrary, simpleRenderer, shaderBinder)
//
private val glRenderer: GLRenderer = GLRenderer().apply {
start("GLUiLayerRenderer")
}
private val mainThreadHandler = Handler(Looper.getMainLooper())
internal val renderableLayer: RenderableRootLayer = RenderableRootLayer(
shaderLibrary = shaderLibrary,
shaderBinder = shaderBinder,
layerDownsampleFactor = downSampleFactor,
density = density,
graphicsLayer = graphicsLayer,
renderer2D = renderer2D,
simpleQuadRenderer = simpleQuadRenderer,
onLayerTextureUpdated = {
renderingPipeline.requestRender {
isRendering.set(false)
semaphore.release()
}
}
)
private val renderingPipeline: RenderingPipeline = RenderingPipeline(
rootLayer = renderableLayer,
simpleRenderer = simpleQuadRenderer,
shaderLibrary = shaderLibrary,
shaderBinder = shaderBinder,
renderer2D = renderer2D,
density = this
)
private val semaphore: java.util.concurrent.Semaphore = java.util.concurrent.Semaphore(1)
private val isGLInitialized = AtomicBoolean(false)
public val isInitialized: MutableState = mutableStateOf(false)
private lateinit var mainRenderTarget: GLRenderer.RenderTarget
private val mainDrawCallback = object : GLRenderer.RenderCallback {
override fun onDrawFrame(eglManager: EGLManager) {
// TODO: implement FPS counter
if (isRendering.compareAndSet(false, true)) {
// renderableLayer.updateTex()
}
}
}
private val isRendering: AtomicBoolean = AtomicBoolean(false)
private val isRecording: AtomicBoolean = AtomicBoolean(false)
internal val isRecorded: Boolean get() = renderableLayer.isReady.not()
init {
initializeIfNeed()
}
private fun initializeIfNeed() {
if (renderableLayer.sizeInt == IntSize.Zero) {
Log.w("UiLayerRenderer", "Warn: GraphicsLayer is empty.")
return
}
if (!isGLInitialized.get()) {
glRenderer.execute {
RenderCommand.init()
// renderer2D.init(assetManager)
simpleRenderer.init()
renderableLayer.initialize()
mainRenderTarget = glRenderer.createRenderTarget(
width = renderableLayer.sizeInt.width,
height = renderableLayer.sizeInt.height,
renderer = mainDrawCallback
)
if (isGLInitialized.compareAndSet(false, true)) {
Snapshot.withMutableSnapshot {
isInitialized.value = true
}
}
}
}
}
context(DrawScope)
public fun recordCanvas(block: DrawScope.() -> Unit): Unit =
trace("UiLayerRenderer#recordCanvas") {
if (isRendering.get() || !isRecording.compareAndSet(false, true)) {
// Rendering is in progress or recording is already in progress, skip recording
logw(TAG, "skipping recordCanvas during rendering")
return
}
try {
trace("recordCanvas") {
if (BuildConfig.DEBUG) {
require(renderableLayer.graphicsLayer.isReleased.not())
}
renderableLayer.graphicsLayer.record(block = block)
}
} finally {
isRecording.set(false)
}
}
@MainThread
public fun onUiLayerUpdated(): Unit = trace("UiLayerRenderer#onUiLayerUpdated") {
initializeIfNeed()
if (isRecording.get()) {
logw(TAG, "skipping onUiLayerUpdated during recording")
return
}
if (isGLInitialized.get()) {
// mainRenderTarget.requestRender()
renderableLayer.updateTex()
// semaphore.acquire()
} else {
isRendering.set(false)
glRenderer.execute {
if (!renderableLayer.isReady && isGLInitialized.get()) {
// mainRenderTarget.requestRender()
mainThreadHandler.post { renderableLayer.updateTex() }
}
}
}
}
private fun attachSurface(
surface: Surface,
id: String,
size: IntSize
): RenderObject {
val existingRenderObject = renderingPipeline.getRenderObject(id)
if (existingRenderObject != null) {
return existingRenderObject
}
val renderObject = RenderObject.createFromSurface(
id = id,
renderableLayer = renderableLayer,
glRenderer = glRenderer,
surface = surface,
rect = Rect(offset = Offset.Zero, size.toSize()),
)
renderingPipeline.addRenderObject(renderObject)
return renderObject
}
internal fun attachRendererSurface(
surface: Surface?,
id: String,
size: IntSize
) {
if (surface != null) {
attachSurface(surface, id, size)
}
}
internal fun detachRenderObject(renderObjectId: String?): Unit = with(renderingPipeline) {
getRenderObject(renderObjectId)?.detachFromRenderer()
removeRenderObject(renderObjectId)
}
public fun destroy() {
if (isInitialized.value) {
isRendering.set(false)
isRecording.set(false)
isInitialized.value = false
glRenderer.stop(true)
renderableLayer.destroy()
renderingPipeline.destroy()
}
}
internal fun updateOffset(renderObjectId: String?, offset: Offset) {
renderingPipeline
.getRenderObject(renderObjectId)
?.updateOffset(offset = offset)
}
internal fun updateStyle(renderObjectId: String?, style: Style) {
renderingPipeline.getRenderObject(renderObjectId)?.style = style
}
internal fun updateMask(renderObjectId: String?, brush: Brush?) {
renderingPipeline.updateMask(glRenderer, renderObjectId, brush)
}
internal companion object {
private const val TAG = "UiLayerRenderer"
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/EffectCoordinator.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer.processing
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.toSize
import androidx.tracing.trace
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
import dev.serhiiyaremych.imla.renderer.framebuffer.Framebuffer
import dev.serhiiyaremych.imla.renderer.RenderCommand
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferPool
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import dev.serhiiyaremych.imla.uirenderer.RenderObject
import dev.serhiiyaremych.imla.uirenderer.RenderableRootLayer
import dev.serhiiyaremych.imla.uirenderer.processing.blend.PostBlendEffect
import dev.serhiiyaremych.imla.uirenderer.processing.blur.DualBlurEffect
import dev.serhiiyaremych.imla.uirenderer.processing.mask.MaskEffect
import dev.serhiiyaremych.imla.uirenderer.processing.noise.NoiseEffect
import dev.serhiiyaremych.imla.uirenderer.processing.preprocess.PreProcessFilter
internal class EffectCoordinator(
density: Density,
private val framebufferPool: FramebufferPool,
private val rootLayer: RenderableRootLayer,
private val simpleQuadRenderer: SimpleQuadRenderer,
private val shaderLibrary: ShaderLibrary,
private val shaderBinder: ShaderBinder
) : Density by density {
private val effectCache: MutableMap = mutableMapOf()
private fun createEffects(): EffectsHolder {
return EffectsHolder(
preProcess = PreProcessFilter(shaderLibrary, framebufferPool, simpleQuadRenderer, shaderBinder),
// blurEffect = BlurEffect(assetManager, simpleQuadRenderer).apply { setup(effectSize) },
blurEffect = DualBlurEffect(framebufferPool, shaderLibrary, shaderBinder, simpleQuadRenderer),
noiseEffect = NoiseEffect(shaderLibrary, shaderBinder, framebufferPool, simpleQuadRenderer),
maskEffect = MaskEffect(shaderLibrary, framebufferPool, shaderBinder, simpleQuadRenderer),
blendEffect = PostBlendEffect(simpleQuadRenderer)
)
}
fun applyEffects(renderObject: RenderObject) = with(Unit) {
val effects = effectCache.getOrPut(renderObject.id) {
createEffects()
}
val maskTexture = renderObject.mask
val (prePrecess, blur, noise, mask, blendEffect) = effects
mask.applyEffect(
backgroundFramebuffer = rootLayer.highResFBO,
backgroundCrop = renderObject.area,
foreground = noise.applyEffect(
texture = blur.applyEffect(
inputFbo = prePrecess.preProcess(rootLayer.highResFBO, renderObject.area),
offset = renderObject.style.offset,
passes = renderObject.style.passes,
tint = renderObject.style.tint
),
noiseAlpha = renderObject.style.noiseAlpha
),
foregroundCrop = prePrecess.contentCrop,
mask = maskTexture
)
val finalFb = output(blur, noise, mask)
if (finalFb != null) {
trace("finalBlendLayers") {
RenderCommand.bindDefaultFramebuffer(Bind.DRAW)
RenderCommand.setViewPort(
x = 0, y = 0,
width = renderObject.area.width.toInt(),
height = renderObject.area.height.toInt()
)
RenderCommand.clear()
blendEffect.blendToDefaultBuffer(
background = rootLayer.highResFBO,
cutBackgroundRegion = renderObject.area,
foreground = finalFb,
cutForegroundRegion = if (maskTexture != null) Rect(
offset = Offset.Zero,
size = finalFb.specification.size.toSize()
) else prePrecess.contentCrop,
opacity = renderObject.style.blurOpacity,
)
}
}
}
private fun output(
blur: DualBlurEffect,
noise: NoiseEffect,
mask: MaskEffect
): Framebuffer? {
return when {
mask.isEnabled() -> mask.outputFramebuffer
noise.isEnabled() -> noise.outputFramebuffer
else -> blur.outputFramebuffer
}
}
fun removeEffectsOf(id: String?) {
effectCache.remove(id)?.dispose()
}
fun destroy() {
effectCache.forEach { (_, effects) ->
effects.dispose()
}
effectCache.clear()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/EffectsHolder.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer.processing
import dev.serhiiyaremych.imla.uirenderer.processing.blend.PostBlendEffect
import dev.serhiiyaremych.imla.uirenderer.processing.blur.DualBlurEffect
import dev.serhiiyaremych.imla.uirenderer.processing.mask.MaskEffect
import dev.serhiiyaremych.imla.uirenderer.processing.noise.NoiseEffect
import dev.serhiiyaremych.imla.uirenderer.processing.preprocess.PreProcessFilter
internal data class EffectsHolder(
val preProcess: PreProcessFilter,
val blurEffect: DualBlurEffect,
val noiseEffect: NoiseEffect,
val maskEffect: MaskEffect,
val blendEffect: PostBlendEffect
) {
fun dispose() {
blurEffect.dispose()
noiseEffect.dispose()
maskEffect.dispose()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/PostProcessingEffect.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.uirenderer.processing
import androidx.compose.ui.unit.IntSize
import dev.serhiiyaremych.imla.renderer.Texture
internal interface PostProcessingEffect {
fun shouldResize(size: IntSize): Boolean
fun setup(size: IntSize)
fun applyEffect(texture: Texture): Texture
fun dispose()
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/SimpleQuadRenderer.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer.processing
import androidx.compose.ui.geometry.Offset
import androidx.tracing.trace
import dev.serhiiyaremych.imla.renderer.shader.Shader
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.SimpleRenderer
import dev.serhiiyaremych.imla.renderer.SubTexture2D
import dev.serhiiyaremych.imla.renderer.Texture
import dev.serhiiyaremych.imla.renderer.Texture2D
import dev.serhiiyaremych.imla.renderer.VertexBuffer
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import dev.serhiiyaremych.imla.renderer.toFloatBuffer
import java.nio.FloatBuffer
internal class SimpleQuadRenderer(
shaderLibrary: ShaderLibrary,
val renderer: SimpleRenderer,
val shaderBinder: ShaderBinder
) {
private var vbo: VertexBuffer? = null
private val simpleQuadShader by lazy(LazyThreadSafetyMode.NONE) {
shaderLibrary
.loadShaderFromFile(vertFileName = "simple_quad", fragFileName = "simple_quad")
.apply {
bindUniformBlock(
SimpleRenderer.TEXTURE_DATA_UBO_BLOCK,
SimpleRenderer.TEXTURE_DATA_UBO_BINDING_POINT
)
}
}
private val simpleDataCache: FloatBuffer by lazy(LazyThreadSafetyMode.NONE) {
FloatArray(renderer.data.textureDataUBO.elements).toFloatBuffer()
}
private var texCoord: Array = Array(4) { Offset.Unspecified }
private var flipY: Boolean = false
private var alpha: Float = -1.0f
fun draw(shader: Shader = simpleQuadShader, texture: Texture? = null, alpha: Float = 1.0f) =
trace("SimpleQuadRenderer#draw") {
renderer.data.vao.bind()
vbo?.bind()
shader.bind(shaderBinder)
if (texture != null) {
texture.bind()
uploadTextureDataIfNeed(alpha, texture.flipTexture, getTextureCoordinates(texture))
}
renderer.flush()
}
fun draw(
shader: Shader = simpleQuadShader,
texture: Texture2D? = null,
textureCoordinates: Array? = null,
alpha: Float = 1.0f
) =
trace("SimpleQuadRenderer#draw") {
renderer.data.vao.bind()
vbo?.bind()
shader.bind(shaderBinder)
if (texture != null) {
texture.bind()
uploadTextureDataIfNeed(
alpha,
texture.flipTexture,
textureCoordinates ?: getTextureCoordinates(texture)
)
}
renderer.flush()
}
private fun uploadTextureDataIfNeed(
alpha: Float,
flipTexture: Boolean,
textureCoordinates: Array
) {
val texCoord: Array = textureCoordinates
val flipY: Boolean = flipTexture
val texCoordinatesChanged = isTexCoordinatesChanged(texCoord)
val flipChanged = isFlipChanged(flipY)
val alphaChanged = isAlphaChanged(alpha)
val isDataChanged =
texCoordinatesChanged || flipChanged || alphaChanged
if (isDataChanged) {
trace("uploadDataIfNeed") {
simpleDataCache.rewind()
// Bottom Left
simpleDataCache.put(texCoord[0].x)
simpleDataCache.put(texCoord[0].y)
simpleDataCache.put(0.0f) // padding
simpleDataCache.put(0.0f) // padding
// Bottom Right
simpleDataCache.put(texCoord[1].x)
simpleDataCache.put(texCoord[1].y)
simpleDataCache.put(0.0f) // padding
simpleDataCache.put(0.0f) // padding
// Top Right
simpleDataCache.put(texCoord[2].x)
simpleDataCache.put(texCoord[2].y)
simpleDataCache.put(0.0f) // padding
simpleDataCache.put(0.0f) // padding
// Top Left
simpleDataCache.put(texCoord[3].x)
simpleDataCache.put(texCoord[3].y)
simpleDataCache.put(0.0f) // padding
simpleDataCache.put(0.0f) // padding
// Flip Y & Alpha
simpleDataCache.put(if (flipY) 1.0f else 0.0f)
simpleDataCache.put(alpha)
simpleDataCache.put(0.0f) // padding
simpleDataCache.put(0.0f) // padding
renderer.data.textureDataUBO.setData(simpleDataCache.position(0))
this.texCoord = texCoord
this.flipY = flipY
this.alpha = alpha
}
}
}
private fun isAlphaChanged(alpha: Float): Boolean {
return this.alpha != alpha
}
private fun isFlipChanged(flipY: Boolean): Boolean {
return this.flipY != flipY
}
private fun isTexCoordinatesChanged(texCoord: Array): Boolean {
return this.texCoord[0] != texCoord[0] || this.texCoord[1] != texCoord[1] || this.texCoord[2] != texCoord[2] || this.texCoord[3] != texCoord[3]
}
private fun getTextureCoordinates(texture: Texture): Array {
return when (texture) {
is Texture2D -> defaultTextureCoords
is SubTexture2D -> texture.texCoords
else -> defaultTextureCoords
}
}
companion object {
private val bottomLeft = Offset(0.0f, 0.0f)
private val bottomRight = Offset(1.0f, 0.0f)
private val topRight = Offset(1.0f, 1.0f)
private val topLeft = Offset(0.0f, 1.0f)
val defaultTextureCoords = arrayOf(bottomLeft, bottomRight, topRight, topLeft) // CCW
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/blend/PostBlendEffect.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer.processing.blend
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.tracing.trace
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
import dev.serhiiyaremych.imla.renderer.framebuffer.Framebuffer
import dev.serhiiyaremych.imla.renderer.RenderCommand
import dev.serhiiyaremych.imla.uirenderer.processing.SimpleQuadRenderer
internal class PostBlendEffect(
private val simpleRenderer: SimpleQuadRenderer
) {
private val cropCoordinates: Array = Array(4) { Offset.Zero }
fun blendToDefaultBuffer(
background: Framebuffer,
cutBackgroundRegion: Rect,
foreground: Framebuffer,
cutForegroundRegion: Rect,
opacity: Float,
) = trace("blendToDefaultBuffer") {
val renderTargetSize = cutBackgroundRegion.size
val clampedBlurOpacity = when {
opacity < 0.1f -> 0.0f
opacity > 0.95f -> 1.0f
else -> opacity
}
when {
clampedBlurOpacity == 0.0f -> { // blur is fully transparent, draw background only
blitBackground(background, cutBackgroundRegion, renderTargetSize)
}
clampedBlurOpacity < 1.0f -> { // blur is translucent, mix background and blur
blendLayers(
background = background,
cutBackgroundRegion = cutBackgroundRegion,
foreground = foreground,
cutForegroundRegion = cutForegroundRegion,
opacity = opacity
)
}
// clampedBlurOpacity == 1.0f
else -> { // blur is fully opaque, draw blur only
blitForeground(foreground, cutForegroundRegion, renderTargetSize)
}
}
}
private fun blitForeground(
foreground: Framebuffer,
cutForegroundRegion: Rect,
renderTargetSize: Size
) = trace("blitForeground") {
foreground.bind(Bind.READ, updateViewport = false)
RenderCommand.bindDefaultFramebuffer(bind = Bind.DRAW)
RenderCommand.setViewPort(
width = renderTargetSize.width.toInt(),
height = renderTargetSize.height.toInt()
)
RenderCommand.blitFramebuffer(
srcX0 = cutForegroundRegion.left.toInt(),
srcY0 = (foreground.specification.size.height - cutForegroundRegion.bottom).toInt(),
srcX1 = cutForegroundRegion.right.toInt(),
srcY1 = cutForegroundRegion.bottom.toInt(),
dstX0 = 0, dstY0 = 0,
dstX1 = renderTargetSize.width.toInt(),
dstY1 = renderTargetSize.height.toInt()
)
}
private fun blitBackground(
background: Framebuffer,
cutBackgroundRegion: Rect,
renderTargetSize: Size
) = trace("blitBackground") {
background.bind(Bind.READ, updateViewport = false)
RenderCommand.bindDefaultFramebuffer(bind = Bind.DRAW)
RenderCommand.setViewPort(
width = renderTargetSize.width.toInt(),
height = renderTargetSize.height.toInt()
)
val cut = cutBackgroundRegion.translate(
translateX = 0f,
translateY = (background.specification.size.height - cutBackgroundRegion.height)
)
RenderCommand.blitFramebuffer(
srcX0 = cut.left.toInt(),
srcY0 = cut.top.toInt(),
srcX1 = cut.right.toInt(),
srcY1 = cut.bottom.toInt(),
dstX0 = 0, dstY0 = 0,
dstX1 = renderTargetSize.width.toInt(),
dstY1 = renderTargetSize.height.toInt()
)
}
private fun blendLayers(
background: Framebuffer,
cutBackgroundRegion: Rect,
foreground: Framebuffer,
cutForegroundRegion: Rect,
opacity: Float
) = trace("blendLayers") {
val backgroundSize = background.specification.size
cropCoordinates[0] = Offset(
x = cutBackgroundRegion.left / backgroundSize.width,
y = cutBackgroundRegion.bottom / backgroundSize.height
) // BL
cropCoordinates[1] = Offset(
x = cutBackgroundRegion.right / backgroundSize.width,
y = cutBackgroundRegion.bottom / backgroundSize.height
) // BR
cropCoordinates[2] = Offset(
x = cutBackgroundRegion.right / backgroundSize.width,
y = cutBackgroundRegion.top / backgroundSize.height
) // TR
cropCoordinates[3] = Offset(
x = cutBackgroundRegion.left / backgroundSize.width,
y = cutBackgroundRegion.top / backgroundSize.height
) // TL
simpleRenderer.draw(
texture = background.colorAttachmentTexture,
textureCoordinates = cropCoordinates
)
val foregroundSize = foreground.specification.size
cropCoordinates[0] = Offset(
x = cutForegroundRegion.left / foregroundSize.width,
y = 1.0f - (cutForegroundRegion.bottom / foregroundSize.height)
) // BL
cropCoordinates[1] = Offset(
x = cutForegroundRegion.right / foregroundSize.width,
y = 1.0f - (cutForegroundRegion.bottom / foregroundSize.height)
) // BR
cropCoordinates[2] = Offset(
x = cutForegroundRegion.right / foregroundSize.width,
y = 1.0f - (cutForegroundRegion.top / foregroundSize.height)
) // TR
cropCoordinates[3] = Offset(
x = cutForegroundRegion.left / foregroundSize.width,
y = 1.0f - (cutForegroundRegion.top / foregroundSize.height)
) // TL
RenderCommand.withBlendingModeEnabled {
simpleRenderer.draw(
texture = foreground.colorAttachmentTexture,
textureCoordinates = cropCoordinates,
alpha = opacity
)
}
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/blur/BlurContext.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer.processing.blur
import androidx.compose.ui.unit.IntSize
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferAttachmentSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferTextureFormat
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import dev.serhiiyaremych.imla.uirenderer.processing.preprocess.times
internal data class BlurContext(
val layerSpecs: List,
val shaderProgram: DualBlurFilterShaderProgram
) {
companion object {
const val PASS_SCALE = 0.67f
const val MAX_PASSES = 4
fun create(
shaderLibrary: ShaderLibrary,
shaderBinder: ShaderBinder,
textureSize: IntSize
): BlurContext {
val fboSpec = FramebufferSpecification(
size = textureSize,
attachmentsSpec = FramebufferAttachmentSpecification.singleColor(format = FramebufferTextureFormat.RGB10_A2)
)
var baseLayerSize = (textureSize * PASS_SCALE).roundToMultipleOfFour()
val fbos = buildList {
for (i in 0..MAX_PASSES) {
add(fboSpec.copy(size = baseLayerSize))
baseLayerSize = (baseLayerSize * PASS_SCALE).roundToMultipleOfFour()
}
}
return BlurContext(
layerSpecs = fbos,
shaderProgram = DualBlurFilterShaderProgram(shaderLibrary, shaderBinder)
)
}
}
}
private fun roundUpToMultipleOfFour(value: Int): Int = (value + 3) / 4 * 4
private fun IntSize.roundToMultipleOfFour(): IntSize =
IntSize(
width = roundUpToMultipleOfFour(this.width),
height = roundUpToMultipleOfFour(this.height)
)
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/blur/DualBlurEffect.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.uirenderer.processing.blur
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.trace
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
import dev.serhiiyaremych.imla.renderer.framebuffer.Framebuffer
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferAttachmentSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferTextureFormat
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferTextureSpecification
import dev.serhiiyaremych.imla.renderer.RenderCommand
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.SubTexture2D
import dev.serhiiyaremych.imla.renderer.Texture
import dev.serhiiyaremych.imla.renderer.Texture2D
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferPool
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import dev.serhiiyaremych.imla.uirenderer.processing.SimpleQuadRenderer
import kotlin.properties.Delegates
// Credits:
// GM Shaders: Blur Philosophy, https://mini.gmshaders.com/p/blur-philosophy
// Bandwidth-efficient graphics, https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph2015_2D00_mmg_2D00_marius_2D00_notes.pdf
internal class DualBlurEffect(
private val framebufferPool: FramebufferPool,
private val shaderLibrary: ShaderLibrary,
private val shaderBinder: ShaderBinder,
private val simpleRenderer: SimpleQuadRenderer
) {
private var resultFramebuffer: Framebuffer by Delegates.notNull()
private var resultFboSpec: FramebufferSpecification by Delegates.notNull()
private var blurContext: BlurContext by Delegates.notNull()
private var isInitialized: Boolean = false
private var reusableFboList: MutableList = ArrayList()
internal val outputFramebuffer: Framebuffer
get() = resultFramebuffer
private fun setup(size: IntSize) {
if (isInitialized.not() || shouldResize(size)) {
trace("setup") {
init(size)
isInitialized = true
}
}
}
private fun shouldResize(size: IntSize): Boolean {
return !isInitialized ||
(resultFramebuffer.specification.size != size)
}
fun applyEffect(
inputFbo: Framebuffer,
@FloatRange(from = 0.1, to = 2.0)
offset: Float,
@IntRange(from = 0, to = BlurContext.MAX_PASSES.toLong())
passes: Int,
tint: Color
): Texture2D = trace("DualKawaseBlurEffect") {
trace("BlurEffect#applyEffect") {
setup(inputFbo.specification.size / inputFbo.specification.downSampleFactor)
resultFramebuffer = framebufferPool.acquire(resultFboSpec)
val enabled = offset > 0.0 && passes > 0
if (passes != reusableFboList.size) {
reusableFboList = ArrayList(passes)
}
for (idx in blurContext.layerSpecs.indices) {
reusableFboList.add(framebufferPool.acquire(blurContext.layerSpecs[idx]))
}
var readFBO: Framebuffer = inputFbo
var drawFBO = if (enabled) reusableFboList[0] else resultFramebuffer
trace("blitFirstFBO") {
readFBO.bind(Bind.READ, false)
drawFBO.bind(Bind.DRAW)
RenderCommand.clear()
RenderCommand.blitFramebuffer(
srcX0 = 0,
srcY0 = 0,
srcX1 = readFBO.specification.size.width,
srcY1 = readFBO.specification.size.height,
dstX0 = 0,
dstY0 = 0,
dstX1 = drawFBO.specification.size.width,
dstY1 = drawFBO.specification.size.height,
mask = RenderCommand.colorBufferBit,
filter = RenderCommand.linearTextureFilter,
)
}
if (enabled.not()) {
return resultFramebuffer.colorAttachmentTexture
}
val shaderProgram = blurContext.shaderProgram
shaderProgram.downShader.bind(shaderBinder)
// Downsample
trace("downsample") {
for (i in 0 until passes) {
readFBO = reusableFboList[i]
drawFBO = reusableFboList[i + 1]
val drawSize = drawFBO.specification.size
val texelX = (1f / drawSize.width) * offset
val texelY = (1f / drawSize.height) * offset
shaderProgram.setTexelSize(
texel = Size(texelX, texelY),
down = true
)
drawFBO.bind(bind = Bind.DRAW)
RenderCommand.clear()
simpleRenderer.draw(
shader = shaderProgram.downShader,
texture = readFBO.colorAttachmentTexture
)
}
}
shaderProgram.upShader.bind(shaderBinder)
// Upsample
trace("upsample") {
for (i in 0 until passes) {
// Upsampling uses buffers in the reverse direction
val readIndex = passes - i
val drawIndex = passes - i - 1
readFBO = reusableFboList[readIndex]
drawFBO = reusableFboList[drawIndex]
drawFBO.bind(Bind.DRAW)
RenderCommand.clear()
val drawSize = drawFBO.specification.size
val texelX = (1f / drawSize.width) * offset
val texelY = (1f / drawSize.height) * offset
shaderProgram.setTexelSize(Size(texelX, texelY), false)
if (drawIndex == 0) {
shaderProgram.setTint(tint)
}
simpleRenderer.draw(shaderProgram.upShader, readFBO.colorAttachmentTexture)
}
}
trace("blitResult") {
blitFramebuffers(
srcFramebuffer = drawFBO,
dstFramebuffer = resultFramebuffer,
srcWidth = drawFBO.colorAttachmentTexture.width,
srcHeight = drawFBO.colorAttachmentTexture.height,
dstWidth = resultFramebuffer.colorAttachmentTexture.width,
dstHeight = resultFramebuffer.colorAttachmentTexture.height
)
}
// trace("clean-up") {
// blurContext.framebuffers.forEach {
// it.bind(updateViewport = false)
// it.invalidateAttachments()
// }
// }
}
return resultFramebuffer.colorAttachmentTexture
}
private fun blitFramebuffers(
srcFramebuffer: Framebuffer,
dstFramebuffer: Framebuffer,
srcWidth: Int,
srcHeight: Int,
dstWidth: Int,
dstHeight: Int
) = trace("blitFramebuffers") {
srcFramebuffer.bind(Bind.READ)
dstFramebuffer.bind(Bind.DRAW)
dstFramebuffer.invalidateAttachments()
RenderCommand.blitFramebuffer(
srcX0 = 0,
srcY0 = 0,
srcX1 = srcWidth,
srcY1 = srcHeight,
dstX0 = 0,
dstY0 = 0,
dstX1 = dstWidth,
dstY1 = dstHeight,
)
}
private fun getSize(texture: Texture): IntSize {
return when (texture) {
is Texture2D -> IntSize(width = texture.width, height = texture.height)
is SubTexture2D -> texture.subTextureSize
else -> error("Unsupported texture: $texture")
}
}
private fun init(size: IntSize) = trace("BlurEffect#init") {
if (isInitialized) {
blurContext.shaderProgram.destroy()
// blurContext.framebuffers.forEach { it.destroy() }
}
resultFboSpec = FramebufferSpecification(
size = size,
attachmentsSpec = FramebufferAttachmentSpecification(
attachments = listOf(FramebufferTextureSpecification(format = FramebufferTextureFormat.RGBA8))
)
)
blurContext = BlurContext.create(shaderLibrary, shaderBinder, size)
}
fun dispose() {
if (isInitialized) {
blurContext.shaderProgram.destroy()
// blurContext.framebuffers.forEach { it.destroy() }
resultFramebuffer.destroy()
isInitialized = false
}
}
companion object {
const val MIN_BLUR_RADIUS_PX = 2
}
private infix fun Offset.shr(i: Int): Offset {
return Offset(
x = (x.toInt() shr i).toFloat(),
y = (y.toInt() shr i).toFloat()
)
}
private operator fun Offset.div(offset: Offset): Offset {
return Offset(
x = this.x / offset.x,
y = this.y / offset.y
)
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/blur/DualBlurFilterShaderProgram.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer.processing.blur
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import dev.romainguy.kotlin.math.Float2
import dev.romainguy.kotlin.math.Float4
import dev.serhiiyaremych.imla.renderer.shader.Shader
import dev.serhiiyaremych.imla.renderer.SimpleRenderer
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import kotlin.properties.Delegates
internal class DualBlurFilterShaderProgram(
shaderLibrary: ShaderLibrary,
private val shaderBinder: ShaderBinder
) {
val downShader: Shader = shaderLibrary.loadShaderFromFile(
vertFileName = "simple_quad",
fragFileName = "blur_down"
).apply {
bind(shaderBinder)
setInt("u_Texture", 0)
bindUniformBlock(
SimpleRenderer.TEXTURE_DATA_UBO_BLOCK,
SimpleRenderer.TEXTURE_DATA_UBO_BINDING_POINT
)
}
val upShader: Shader = shaderLibrary.loadShaderFromFile(
vertFileName = "simple_quad",
fragFileName = "blur_up"
).apply {
bind(shaderBinder)
setInt("u_Texture", 0)
bindUniformBlock(
SimpleRenderer.TEXTURE_DATA_UBO_BLOCK,
SimpleRenderer.TEXTURE_DATA_UBO_BINDING_POINT
)
}
private var down: Boolean = true
private var contentOffset: Offset by Delegates.observable(Offset.Zero) { _, old, new ->
if (old != new && new != Offset.Zero) {
val shader = if (down) downShader else upShader
shader.setFloat2("u_ContentOffset", Float2(new.x, new.y))
}
}
private var halfPixel: Size by Delegates.observable(Size.Zero) { _, old, new ->
if (old != new && new != Size.Zero) {
val shader = if (down) downShader else upShader
shader.setFloat2("u_Texel", Float2(new.width, new.height))
}
}
private var tintColor: Color by Delegates.observable(Color.Transparent) { _, old, new ->
if (old != new) {
upShader.setFloat4("u_Tint", Float4(new.red, new.green, new.blue, new.alpha))
}
}
fun setContentOffset(offset: Offset, down: Boolean) {
if (this.down != down) {
this.contentOffset = Offset.Zero
}
this.down = down
this.contentOffset = offset
}
fun setTexelSize(texel: Size, down: Boolean) {
this.down = down
this.halfPixel = texel
}
fun setTint(color: Color) {
this.tintColor = color
}
fun destroy() {
downShader.destroy()
upShader.destroy()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/blur/SepGaussianBlurEffect.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.uirenderer.processing.blur
import android.content.res.AssetManager
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.trace
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
import dev.serhiiyaremych.imla.renderer.framebuffer.Framebuffer
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferAttachmentSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferTextureFormat
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferTextureSpecification
import dev.serhiiyaremych.imla.renderer.RenderCommand
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.SubTexture2D
import dev.serhiiyaremych.imla.renderer.Texture
import dev.serhiiyaremych.imla.renderer.Texture2D
import dev.serhiiyaremych.imla.uirenderer.processing.SimpleQuadRenderer
import kotlin.properties.Delegates
// GM Shaders: Blur Philosophy, https://mini.gmshaders.com/p/blur-philosophy
internal class SepGaussianBlurEffect(
assetManager: AssetManager,
shaderBinder: ShaderBinder,
private val simpleRenderer: SimpleQuadRenderer
) {
private val blurShaderProgram: SimpleBlurShaderProgram =
SimpleBlurShaderProgram(assetManager, shaderBinder)
private var extraHPassFramebuffer: Framebuffer by Delegates.notNull()
private var extraVPassFramebuffer: Framebuffer by Delegates.notNull()
private var horizontalPassFramebuffer: Framebuffer by Delegates.notNull()
private var verticalPassFramebuffer: Framebuffer by Delegates.notNull()
private var resultFramebuffer: Framebuffer by Delegates.notNull()
private var blurRadius: Float = 0f
private var isInitialized: Boolean = false
internal val outputFramebuffer: Framebuffer
get() = resultFramebuffer
fun setup(size: IntSize) {
if (isInitialized.not() || shouldResize(size)) {
trace("setup") {
init(size)
isInitialized = true
}
}
}
private fun shouldResize(size: IntSize): Boolean {
return !isInitialized ||
(horizontalPassFramebuffer.specification.size != size || verticalPassFramebuffer.specification.size != size)
}
fun applyEffect(texture: Texture, blurRadius: Float, tint: Color): Texture {
trace("BlurEffect#applyEffect") {
val effectSize = getSize(texture)
setup(effectSize)
this.blurRadius = blurRadius
if (isEnabled().not()) {
return@trace texture
}
trace("setStyle") {
blurShaderProgram.setBlurRadius(blurRadius)
blurShaderProgram.setTintColor(tint)
blurShaderProgram.setTexSize(effectSize)
}
// horizontal pass
trace("horizontalPass") {
blurShaderProgram.setHorizontalPass()
horizontalPassFramebuffer.bind(Bind.DRAW)
RenderCommand.clear()
simpleRenderer.draw(
shader = blurShaderProgram.shader,
texture = texture
)
}
// vertical pass
trace("verticalPass") {
blurShaderProgram.setVerticalPass()
verticalPassFramebuffer.bind(Bind.DRAW)
RenderCommand.clear()
simpleRenderer.draw(
shader = blurShaderProgram.shader,
texture = horizontalPassFramebuffer.colorAttachmentTexture
)
}
if (DO_EXTRA_BLURRING_PASS) {
trace("extraHPass") {
extraHPassFramebuffer.bind(Bind.DRAW)
RenderCommand.clear()
blurShaderProgram.setHorizontalPass()
simpleRenderer.draw(
shader = blurShaderProgram.shader,
texture = verticalPassFramebuffer.colorAttachmentTexture
)
}
trace("extraVPass") {
extraVPassFramebuffer.bind(Bind.DRAW)
RenderCommand.clear()
blurShaderProgram.setVerticalPass()
simpleRenderer.draw(
shader = blurShaderProgram.shader,
texture = extraHPassFramebuffer.colorAttachmentTexture
)
}
}
trace("blitResult") {
val srcFramebuffer = if (DO_EXTRA_BLURRING_PASS) {
extraVPassFramebuffer
} else {
verticalPassFramebuffer
}
blitFramebuffers(
srcFramebuffer = srcFramebuffer,
dstFramebuffer = resultFramebuffer,
srcWidth = srcFramebuffer.colorAttachmentTexture.width,
srcHeight = srcFramebuffer.colorAttachmentTexture.height,
dstWidth = resultFramebuffer.colorAttachmentTexture.width,
dstHeight = resultFramebuffer.colorAttachmentTexture.height
)
}
}
return resultFramebuffer.colorAttachmentTexture
}
private fun blitFramebuffers(
srcFramebuffer: Framebuffer,
dstFramebuffer: Framebuffer,
srcWidth: Int,
srcHeight: Int,
dstWidth: Int,
dstHeight: Int
) = trace("blitFramebuffers") {
srcFramebuffer.bind(Bind.READ)
dstFramebuffer.bind(Bind.DRAW)
RenderCommand.clear()
RenderCommand.blitFramebuffer(
srcX0 = 0,
srcY0 = 0,
srcX1 = srcWidth,
srcY1 = srcHeight,
dstX0 = 0,
dstY0 = 0,
dstX1 = dstWidth,
dstY1 = dstHeight,
)
}
private fun getSize(texture: Texture): IntSize {
return when (texture) {
is Texture2D -> IntSize(width = texture.width, height = texture.height)
is SubTexture2D -> texture.subTextureSize
else -> error("Unsupported texture: $texture")
}
}
private fun init(size: IntSize) = trace("BlurEffect#init") {
if (isInitialized) {
horizontalPassFramebuffer.destroy()
verticalPassFramebuffer.destroy()
}
val spec = FramebufferSpecification(
size = size,
attachmentsSpec = FramebufferAttachmentSpecification(
attachments = listOf(FramebufferTextureSpecification(format = FramebufferTextureFormat.RGBA8))
)
)
horizontalPassFramebuffer = Framebuffer.create(spec)
verticalPassFramebuffer = Framebuffer.create(spec)
resultFramebuffer = Framebuffer.create(spec)
if (DO_EXTRA_BLURRING_PASS) {
val extraPassSpec = spec.copy(downSampleFactor = spec.downSampleFactor * 2)
extraHPassFramebuffer = Framebuffer.create(extraPassSpec)
extraVPassFramebuffer = Framebuffer.create(extraPassSpec)
}
}
fun isEnabled(): Boolean = blurRadius > MIN_BLUR_RADIUS_PX
fun dispose() {
horizontalPassFramebuffer.destroy()
verticalPassFramebuffer.destroy()
blurShaderProgram.destroy()
resultFramebuffer.destroy()
if (DO_EXTRA_BLURRING_PASS) {
extraHPassFramebuffer.destroy()
extraVPassFramebuffer.destroy()
}
isInitialized = false
}
companion object {
const val MIN_BLUR_RADIUS_PX = 2
const val DO_EXTRA_BLURRING_PASS = false
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/blur/SimpleBlurShaderProgram.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("unused")
package dev.serhiiyaremych.imla.uirenderer.processing.blur
import android.content.res.AssetManager
import androidx.annotation.FloatRange
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntSize
import dev.romainguy.kotlin.math.Float2
import dev.romainguy.kotlin.math.Float4
import dev.serhiiyaremych.imla.renderer.shader.Shader
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.SimpleRenderer
import kotlin.properties.Delegates
internal class SimpleBlurShaderProgram(assetManager: AssetManager, private val shaderBinder: ShaderBinder) {
val shader: Shader = Shader.create(
assetManager = assetManager,
vertexAsset = "shader/simple_quad.vert",
fragmentAsset = "shader/simple_blur.frag",
).apply {
bind(shaderBinder)
setInt("u_Texture", 0)
bindUniformBlock(
SimpleRenderer.TEXTURE_DATA_UBO_BLOCK,
SimpleRenderer.TEXTURE_DATA_UBO_BINDING_POINT
)
}
private var direction: Float2 by Delegates.observable(zeroDir) { _, old, new ->
if (old != new && new != zeroDir) {
shader.bind(shaderBinder)
shader.setFloat2("u_BlurDirection", new)
}
}
private var blurRadiusPx: Float by Delegates.observable(0f) { _, old, new ->
if (old != new && new > 0) {
shader.bind(shaderBinder)
val clampedRadius = new.coerceIn(1f, 72f)
shader.setFloat("u_BlurSigma", clampedRadius)
}
}
private var blurTintColor: Color by Delegates.observable(Color.Unspecified) { _, old, new ->
if (old != new && new != Color.Unspecified) {
shader.bind(shaderBinder)
shader.setFloat4("u_BlurTint", Float4(new.red, new.green, new.blue, new.alpha))
}
}
private var blurTexSize: IntSize by Delegates.observable(IntSize.Zero) { _, old, new ->
if (old != new && new != IntSize.Zero) {
shader.bind(shaderBinder)
shader.setFloat2("u_TexelSize", Float2(new.width.toFloat(), new.height.toFloat()))
}
}
fun setBlurRadius(@FloatRange(from = 1.0, to = 72.0) radius: Float) {
blurRadiusPx = radius
}
// horiz=(1.0, 0.0), vert=(0.0, 1.0)
fun setHorizontalPass() {
direction = horizontalDirection
}
fun setVerticalPass() {
direction = verticalDirection
}
fun setTintColor(tint: Color) {
blurTintColor = tint
}
fun setTexSize(size: IntSize) {
blurTexSize = size
}
fun destroy() {
shader.destroy()
}
private companion object {
val zeroDir = Float2()
val horizontalDirection = Float2(x = 1.0f)
val verticalDirection = Float2(y = 1.0f)
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/mask/MaskEffect.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer.processing.mask
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.unit.IntSize
import androidx.tracing.trace
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
import dev.serhiiyaremych.imla.renderer.framebuffer.Framebuffer
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferAttachmentSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferSpecification
import dev.serhiiyaremych.imla.renderer.MAX_TEXTURE_SLOTS
import dev.serhiiyaremych.imla.renderer.RenderCommand
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.Texture
import dev.serhiiyaremych.imla.renderer.Texture2D
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferPool
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import dev.serhiiyaremych.imla.uirenderer.processing.SimpleQuadRenderer
internal class MaskEffect(
shaderLibrary: ShaderLibrary,
private val framebufferPool: FramebufferPool,
private val shaderBinder: ShaderBinder,
private val simpleQuadRenderer: SimpleQuadRenderer
) {
private val shaderProgram = MaskShaderProgram(shaderLibrary, shaderBinder)
private lateinit var cropBackgroundFramebuffer: Framebuffer
private lateinit var cropBackgroundFramebufferSpec: FramebufferSpecification
private lateinit var finalMaskFrameBuffer: Framebuffer
private lateinit var finalMaskFrameBufferSpec: FramebufferSpecification
private var isInitialized: Boolean = false
private var maskTexture: Texture? = null
private val cropCoordinates: Array = Array(4) { Offset.Zero }
internal val outputFramebuffer: Framebuffer
get() = finalMaskFrameBuffer
private fun shouldResize(size: IntSize): Boolean {
return !isInitialized || (finalMaskFrameBuffer.specification.size != size)
}
private fun setup(size: IntSize) {
if (shouldResize(size)) {
if (isInitialized) {
finalMaskFrameBuffer.destroy()
cropBackgroundFramebuffer.destroy()
}
finalMaskFrameBufferSpec = FramebufferSpecification(
size = size,
attachmentsSpec = FramebufferAttachmentSpecification()
)
// finalMaskFrameBuffer = Framebuffer.create(spec)
isInitialized = true
// cropBackgroundFramebuffer = Framebuffer.create(spec)
val samplers = IntArray(MAX_TEXTURE_SLOTS) { index -> index }
shaderProgram.shader.bind(shaderBinder)
shaderProgram.shader.setIntArray("u_Textures", samplers)
}
}
fun applyEffect(
backgroundFramebuffer: Framebuffer,
backgroundCrop: Rect,
foreground: Texture2D,
foregroundCrop: Rect,
mask: Texture2D?
) =
trace("MaskEffect#applyEffect") {
maskTexture = mask
if (isEnabled()) {
requireNotNull(mask)
setup(IntSize(mask.width, mask.height))
trace("cutBackgroundRegion") {
cropBackgroundFramebuffer = framebufferPool.acquire(cropBackgroundFramebufferSpec)
finalMaskFrameBuffer = framebufferPool.acquire(finalMaskFrameBufferSpec)
backgroundFramebuffer.bind(Bind.READ)
cropBackgroundFramebuffer.bind(Bind.DRAW)
RenderCommand.clear()
val crop = backgroundCrop.translate(
translateX = 0f,
translateY = backgroundFramebuffer.specification.size.height - backgroundCrop.height
)
RenderCommand.blitFramebuffer(
srcX0 = crop.left.toInt(),
srcY0 = crop.top.toInt(),
srcX1 = crop.width.toInt(),
srcY1 = crop.bottom.toInt(),
dstX0 = 0,
dstY0 = 0,
dstX1 = cropBackgroundFramebuffer.specification.size.width,
dstY1 = cropBackgroundFramebuffer.specification.size.height,
mask = RenderCommand.colorBufferBit,
filter = RenderCommand.linearTextureFilter,
)
}
trace("setMaskProp") {
shaderProgram.setMask(mask)
shaderProgram.setBackground(cropBackgroundFramebuffer.colorAttachmentTexture)
}
trace("drawMask") {
finalMaskFrameBuffer.bind(Bind.DRAW)
RenderCommand.clear()
val foregroundSize = IntSize(foreground.width, foreground.height)
cropCoordinates[0] = Offset(
x = foregroundCrop.left / foregroundSize.width,
y = 1.0f - (foregroundCrop.bottom / foregroundSize.height)
) // BL
cropCoordinates[1] = Offset(
x = foregroundCrop.right / foregroundSize.width,
y = 1.0f - (foregroundCrop.bottom / foregroundSize.height)
) // BR
cropCoordinates[2] = Offset(
x = foregroundCrop.right / foregroundSize.width,
y = 1.0f - (foregroundCrop.top / foregroundSize.height)
) // TR
cropCoordinates[3] = Offset(
x = foregroundCrop.left / foregroundSize.width,
y = 1.0f - (foregroundCrop.top / foregroundSize.height)
) // TL
simpleQuadRenderer.draw(
shader = shaderProgram.shader,
texture = foreground,
textureCoordinates = cropCoordinates
)
}
}
}
fun isEnabled(): Boolean = maskTexture != null
fun dispose() {
if (isInitialized) {
finalMaskFrameBuffer.destroy()
isInitialized = false
}
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/mask/MaskShaderProgram.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer.processing.mask
import dev.serhiiyaremych.imla.renderer.shader.Shader
import dev.serhiiyaremych.imla.renderer.SimpleRenderer
import dev.serhiiyaremych.imla.renderer.Texture
import dev.serhiiyaremych.imla.renderer.Texture2D
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
internal class MaskShaderProgram(
shaderLibrary: ShaderLibrary,
private val shaderBinder: ShaderBinder
) {
val shader: Shader = shaderLibrary
.loadShaderFromFile(vertFileName = "simple_quad", fragFileName = "simple_mask")
.apply {
bindUniformBlock(
SimpleRenderer.TEXTURE_DATA_UBO_BLOCK,
SimpleRenderer.TEXTURE_DATA_UBO_BINDING_POINT
)
}
fun setMask(mask: Texture2D) {
shader.bind(shaderBinder)
mask.bind(2)
shader.setInt("u_Mask", 2)
}
fun setBackground(background: Texture) {
shader.bind(shaderBinder)
background.bind(3)
shader.setInt("u_Background", 3)
}
fun destroy() {
shader.destroy()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/noise/NoiseEffect.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer.processing.noise
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.trace
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
import dev.serhiiyaremych.imla.renderer.framebuffer.Framebuffer
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferAttachmentSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferSpecification
import dev.serhiiyaremych.imla.renderer.RenderCommand
import dev.serhiiyaremych.imla.renderer.shader.Shader
import dev.serhiiyaremych.imla.renderer.SimpleRenderer
import dev.serhiiyaremych.imla.renderer.Texture2D
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferPool
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import dev.serhiiyaremych.imla.uirenderer.processing.SimpleQuadRenderer
import kotlin.properties.Delegates
internal class NoiseEffect(
shaderLibrary: ShaderLibrary,
shaderBinder: ShaderBinder,
private val framebufferPool: FramebufferPool,
private val simpleQuadRenderer: SimpleQuadRenderer
) {
private val shader: Shader = shaderLibrary.loadShaderFromFile(
vertFileName = "simple_quad",
fragFileName = "noise",
).apply {
bind(shaderBinder)
bindUniformBlock(
SimpleRenderer.TEXTURE_DATA_UBO_BLOCK,
SimpleRenderer.TEXTURE_DATA_UBO_BINDING_POINT
)
}
private var noiseTextureFrameBuffer: Framebuffer by Delegates.notNull()
private var effectFrameBuffer: Framebuffer by Delegates.notNull()
private var effectFrameSpec: FramebufferSpecification by Delegates.notNull()
private var isNoiseTextureInitialized: Boolean = false
private var isNoiseTextureDrawn: Boolean = false
private var noiseAlpha: Float = 0.0f
internal val outputFramebuffer: Framebuffer
get() = effectFrameBuffer
private fun setup(size: IntSize) {
if (shouldResize(size)) {
init(size)
}
}
private fun init(size: IntSize) = trace("init") {
if (isNoiseTextureInitialized) {
noiseTextureFrameBuffer.destroy()
effectFrameBuffer.destroy()
}
effectFrameSpec = FramebufferSpecification(
size = size,
attachmentsSpec = FramebufferAttachmentSpecification()
)
noiseTextureFrameBuffer = Framebuffer.create(effectFrameSpec)
isNoiseTextureInitialized = true
}
private fun shouldResize(size: IntSize): Boolean {
return !isNoiseTextureInitialized || noiseTextureFrameBuffer.specification.size != size
}
private fun drawNoiseTextureOnce() {
if (!isNoiseTextureDrawn) {
trace("drawNoiseTextureOnce") {
noiseTextureFrameBuffer.bind(Bind.DRAW)
simpleQuadRenderer.draw(shader = shader)
}
isNoiseTextureDrawn = true
}
}
fun applyEffect(texture: Texture2D, noiseAlpha: Float): Texture2D {
this.noiseAlpha = noiseAlpha
if (isEnabled()) {
trace("NoiseEffect#applyEffect") {
setup(IntSize(width = texture.width, height = texture.height))
drawNoiseTextureOnce()
effectFrameBuffer = framebufferPool.acquire(effectFrameSpec)
effectFrameBuffer.bind(Bind.DRAW)
RenderCommand.clear()
RenderCommand.enableBlending()
simpleQuadRenderer.draw(texture = texture)
simpleQuadRenderer.draw(
texture = noiseTextureFrameBuffer.colorAttachmentTexture,
alpha = noiseAlpha
)
}
RenderCommand.disableBlending()
return effectFrameBuffer.colorAttachmentTexture
} else {
return texture
}
}
fun isEnabled(): Boolean = noiseAlpha >= MIN_NOISE_ALPHA
fun dispose() {
if (isNoiseTextureInitialized) {
noiseTextureFrameBuffer.destroy()
}
isNoiseTextureInitialized = false
}
private companion object {
const val MIN_NOISE_ALPHA = 0.05f
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/noise/NoiseShaderProgram.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer.processing.noise
import android.content.res.AssetManager
import dev.serhiiyaremych.imla.renderer.BufferLayout
import dev.serhiiyaremych.imla.renderer.shader.Shader
import dev.serhiiyaremych.imla.renderer.shader.ShaderProgram
import dev.serhiiyaremych.imla.renderer.objects.defaultQuadBufferLayout
import dev.serhiiyaremych.imla.renderer.objects.defaultQuadVertexMapper
import dev.serhiiyaremych.imla.renderer.primitive.QuadVertex
internal class NoiseShaderProgram(assetManager: AssetManager) : ShaderProgram {
override val shader: Shader = Shader.create(
assetManager = assetManager,
vertexAsset = "shader/default_quad.vert",
fragmentAsset = "shader/noise.frag",
)
override val vertexBufferLayout: BufferLayout = defaultQuadBufferLayout
override val componentsCount: Int = vertexBufferLayout.elements.sumOf { it.type.components }
override fun mapVertexData(quadVertexBufferBase: List) =
defaultQuadVertexMapper(quadVertexBufferBase)
override fun destroy() {
shader.destroy()
}
}
================================================
FILE: imla/src/main/java/dev/serhiiyaremych/imla/uirenderer/processing/preprocess/PreProcessFilter.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla.uirenderer.processing.preprocess
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toIntSize
import androidx.compose.ui.unit.toSize
import androidx.tracing.trace
import dev.romainguy.kotlin.math.Float2
import dev.serhiiyaremych.imla.renderer.framebuffer.Bind
import dev.serhiiyaremych.imla.renderer.framebuffer.Framebuffer
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferAttachmentSpecification
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferSpecification
import dev.serhiiyaremych.imla.renderer.RenderCommand
import dev.serhiiyaremych.imla.renderer.shader.ShaderBinder
import dev.serhiiyaremych.imla.renderer.SimpleRenderer
import dev.serhiiyaremych.imla.renderer.framebuffer.FramebufferPool
import dev.serhiiyaremych.imla.renderer.shader.ShaderLibrary
import dev.serhiiyaremych.imla.renderer.util.SizeUtil
import dev.serhiiyaremych.imla.uirenderer.processing.SimpleQuadRenderer
import org.intellij.lang.annotations.Language
import kotlin.properties.Delegates
@Language("GLSL")
private const val preProcessFragmentSource = """
#version 300 es
precision mediump float;
uniform sampler2D u_Texture;
uniform vec2 u_TexelSize;
uniform vec2 u_ContentSize;
in vec2 maskCoord;
in vec2 texCoord;
out vec4 color;
const vec2 center = vec2(0.5);
const vec3 luma = vec3(0.3, 0.59, 0.11);
// Credits: Bart Wronski, https://bartwronski.com/2022/03/07/fast-gpu-friendly-antialiasing-downsampling-filter/
const vec2 OFFSETS[8] = vec2[8](
vec2(-0.95777, -0.95777),
vec2(0.95777, -0.95777),
vec2(0.95777, 0.95777),
vec2(-0.95777, 0.95777),
vec2(-3.907, 0.0),
vec2(3.907, 0.0),
vec2(0.0, -3.907),
vec2(0.0, 3.907)
);
// const vec2 OFFSETS[8] = vec2[8](
// vec2(-0.75777, -0.75777),
// vec2(0.75777, -0.75777),
// vec2(0.75777, 0.75777),
// vec2(-0.75777, 0.75777),
// vec2(-2.907, 0.0),
// vec2(2.907, 0.0),
// vec2(0.0, -2.907),
// vec2(0.0, 2.907)
// );
const float WEIGHTS[8] = float[8](
0.37487566,
0.37487566,
0.37487566,
0.37487566,
-0.12487566,
-0.12487566,
-0.12487566,
-0.12487566
);
vec3 desaturate(vec3 color, float saturation) {
// Convert color to grayscale (luminosity method)
float gray = dot(color, luma);
return mix(vec3(gray), color, saturation);
}
vec2 calculateClampedTexCoord(vec2 coord) {
vec2 halfSize = u_ContentSize * 0.5;
vec2 rectMin = center - halfSize;
vec2 rectMax = center + halfSize;
vec2 rectSize = u_ContentSize;
vec2 texCoord = clamp((coord - rectMin) / rectSize, 0.0, 1.0);
return texCoord;
}
float sdfRoundRect(vec2 point, vec2 rectSize, vec2 cornerRadius) {
cornerRadius = min(cornerRadius, rectSize);
vec2 dist = abs(point) - rectSize + cornerRadius;
float outsideDistance = length(max(dist, 0.0));
float insideDistance = min(max(dist.x, dist.y), 0.0);
return outsideDistance + insideDistance - length(cornerRadius);
}
float calculateBlendFactor(vec2 coord) {
vec2 uv = coord * 2.0 - 1.0;
highp vec2 corners = vec2(0.01);
return sdfRoundRect(uv, u_ContentSize - 0.01, corners);
}
vec3 applyChromaticAberration(sampler2D tex, vec2 uv, vec2 clampedUV) {
vec2 distFromCenter = (center-uv);
float circleSDF = (1.-smoothstep(0.0, max(u_ContentSize.x, u_ContentSize.y), length(distFromCenter)));
vec2 offset = circleSDF*(u_TexelSize*5.);
vec3 color;
color.r = texture(tex, clampedUV + offset).r;
color.g = texture(tex, clampedUV).g;
color.b = texture(tex, clampedUV - offset).b;
return color;
}
vec4 sampleTexture(sampler2D tex, vec2 uv) {
float blendFactor = calculateBlendFactor(uv);
vec2 clampledUV = calculateClampedTexCoord(uv);
vec4 sharpSample = texture(tex, clampledUV);
vec3 color = applyChromaticAberration(tex, uv, clampledUV);
sharpSample = vec4(color, 1.0);
float mipLevel = 5.0;
vec4 mipSample = textureLod(tex, clampledUV, mipLevel);
// debug draw sdf mask
// vec3 rectColor = vec3(0.1, 0.4, 0.7); // Rectangle color
// vec3 bgColor = vec3(1.0); // Background color
// vec4 sdfMaskColor = vec4(mix(bgColor, rectColor, alpha), 1.0);
// Edge smoothing
float alpha = 1.0 - smoothstep(-0.03, 0.0, blendFactor);
return mix(vec4(desaturate(mipSample.rgb, alpha+0.4), 1.0), sharpSample, alpha);
}
void main() {
vec4 baseColor = vec4(0.0);
float totalWeight = 0.0;
for (int i = 0; i < 8; i++) {
vec2 sampleCoord = texCoord + OFFSETS[i] * u_TexelSize;
vec4 sampleColor = sampleTexture(u_Texture, sampleCoord);
baseColor += WEIGHTS[i] * sampleColor;
totalWeight += WEIGHTS[i];
}
totalWeight = max(totalWeight, 1e-5); // Prevent division by zero
baseColor /= totalWeight;
color = baseColor;
}
"""
internal class PreProcessFilter(
shaderLibrary: ShaderLibrary,
private val framebufferPool: FramebufferPool,
private val simpleQuadRenderer: SimpleQuadRenderer,
private val shaderBinder: ShaderBinder
) {
private val preProcessShader = shaderLibrary.loadShader(
name = "preProcess",
fragmentSrc = preProcessFragmentSource.trimIndent(),
).apply {
bind(shaderBinder)
setInt("u_Texture", 0)
bindUniformBlock(
SimpleRenderer.TEXTURE_DATA_UBO_BLOCK,
SimpleRenderer.TEXTURE_DATA_UBO_BINDING_POINT
)
}
private var cutFboSpec: FramebufferSpecification by Delegates.notNull()
private var targetFboSpec: FramebufferSpecification by Delegates.notNull()
private var isInitialized: Boolean = false
var contentCrop = Rect.Zero
private set
var leftExtend: Float = 0f
var topExtend: Float = 0f
var rightExtend: Float = 0f
var bottomExtend: Float = 0f
private var extendedContentBounds: Rect = Rect.Zero
fun preProcess(rootFbo: Framebuffer, area: Rect): Framebuffer = trace("preProcess") {
init(rootFbo, area)
val target = framebufferPool.acquire(targetFboSpec)
val cut = framebufferPool.acquire(cutFboSpec)
cutArea(rootFbo, cut)
val targetSize = target.specification.size / target.specification.downSampleFactor
val textureSize = cut.specification.size
trace("aaDownsampling") {
target.bind(Bind.DRAW)
RenderCommand.clear()
preProcessShader.bind(shaderBinder)
preProcessShader.setFloat2(
name = "u_TexelSize",
value = Float2(x = 1.0f / targetSize.width, y = 1.0f / targetSize.height)
)
val scale = calculateFitScale(inner = textureSize, outer = targetSize)
val scaledWidth = textureSize.width * scale
val scaledHeight = textureSize.height * scale
val normalizedContentBounds = Float2(
x = scaledWidth / targetSize.width.toFloat(),
y = scaledHeight / targetSize.height.toFloat(),
)
preProcessShader.setFloat2(
name = "u_ContentSize",
value = normalizedContentBounds
)
simpleQuadRenderer.draw(shader = preProcessShader, cut.colorAttachmentTexture)
target.colorAttachmentTexture.bind()
target.colorAttachmentTexture.generateMipMaps()
}
return target
}
private fun calculateFitScale(inner: IntSize, outer: IntSize): Float {
val innerIsBigger = (inner.width * inner.height) > (outer.width * outer.height)
val widthScale = outer.width / inner.width.toFloat()
val heightScale = outer.height / inner.height.toFloat()
val scale = minOf(widthScale, heightScale)
return if (innerIsBigger || scale > 1f) scale else 1f
}
private fun cutArea(
src: Framebuffer,
dst: Framebuffer
) = trace("cutArea") {
src.bind(Bind.READ, updateViewport = false)
dst.bind(Bind.DRAW)
RenderCommand.blitFramebuffer(
srcX0 = extendedContentBounds.left.toInt(),
srcX1 = extendedContentBounds.right.toInt(),
srcY0 = src.specification.size.height - (extendedContentBounds.top).toInt(),
srcY1 = src.specification.size.height - (extendedContentBounds.bottom).toInt(),
dstX0 = 0, dstX1 = dst.specification.size.width.toInt(),
dstY0 = 0, dstY1 = dst.specification.size.height.toInt()
)
trace("generateMipMaps") {
dst.colorAttachmentTexture.bind()
dst.colorAttachmentTexture.generateMipMaps()
}
}
private fun init(rootFbo: Framebuffer, area: Rect) {
if (isInitialized.not()) {
leftExtend = minOf(EXPAND_CONTENT_PX, area.left)
rightExtend =
minOf(EXPAND_CONTENT_PX, rootFbo.specification.size.width - area.right)
topExtend = minOf(EXPAND_CONTENT_PX, area.top)
bottomExtend =
minOf(EXPAND_CONTENT_PX, rootFbo.specification.size.height - area.bottom)
extendedContentBounds = Rect(
topLeft = Offset(
x = (area.left - leftExtend),
y = (area.top - topExtend)
),
bottomRight = Offset(
x = (area.right + rightExtend),
y = (area.bottom + bottomExtend)
)
)
val extendedSize = extendedContentBounds.size.toIntSize()
val spec = FramebufferSpecification(
size = extendedSize,
attachmentsSpec = FramebufferAttachmentSpecification.singleColor(flip = true)
)
cutFboSpec = spec.copy(
attachmentsSpec = FramebufferAttachmentSpecification.singleColor(
mipmapFiltering = true,
flip = true
)
)
// Framebuffer.create(cutSpec)
val potUpSize = SizeUtil.closestPOTUp(extendedSize)
// render target if half size of pot input texture
targetFboSpec = spec.copy(size = potUpSize * 0.5f)
//Framebuffer.create(spec.copy(size = potUpSize * 0.5f))
val targetSize = targetFboSpec.size
val fitScale = calculateFitScale(
inner = extendedContentBounds.size.toIntSize(),
outer = targetSize
)
val fitToScaleSize = IntSize(
width = (extendedContentBounds.size.width * fitScale).toInt(),
height = (extendedContentBounds.size.height * fitScale).toInt()
)
val centeredBounds = Rect(
offset = Offset(
x = (targetSize.width - fitToScaleSize.width) / 2f,
y = (targetSize.height - fitToScaleSize.height) / 2f
),
size = fitToScaleSize.toSize()
)
contentCrop = Rect(
left = centeredBounds.left + (leftExtend * fitScale),
top = centeredBounds.top + (topExtend * fitScale),
right = centeredBounds.right - (rightExtend * fitScale),
bottom = centeredBounds.bottom - (bottomExtend * fitScale),
)
isInitialized = true
}
}
companion object {
const val EXPAND_CONTENT_PX = 20f
}
}
internal operator fun Rect.times(value: Float): Rect {
return Rect(
topLeft = Offset(
x = left * value,
y = top * value
),
bottomRight = Offset(
x = right * value,
y = bottom * value
)
)
}
internal operator fun IntSize.times(value: Float): IntSize {
return IntSize(
width = (this.width * value).toInt(),
height = (this.height * value).toInt()
)
}
================================================
FILE: imla/src/test/java/dev/serhiiyaremych/imla/ExampleUnitTest.kt
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
package dev.serhiiyaremych.imla
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
================================================
FILE: settings.gradle.kts
================================================
/*
* Copyright 2024, Serhii Yaremych
* SPDX-License-Identifier: MIT
*/
@file:Suppress("UnstableApiUsage")
include(":benchmark")
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "imla-android"
include(":app")
include(":imla")