diff --git a/app/build.gradle b/app/build.gradle
index f0d7f9ab8..14aaa11a5 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -6,7 +6,7 @@ apply plugin: 'kotlin-kapt'
def getGitSha = { ->
def stdout = new ByteArrayOutputStream()
exec {
- commandLine 'git', 'rev-parse', '--short' , 'HEAD'
+ commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = stdout
}
return stdout.toString().trim()
@@ -35,15 +35,15 @@ android {
shrinkResources true
proguardFiles 'proguard-rules.pro'
}
- debug { }
+ debug {}
}
flavorDimensions "color"
productFlavors {
- blue { }
+ blue {}
green {
applicationIdSuffix ".test"
- versionNameSuffix "-"+getGitSha()
+ versionNameSuffix "-" + getGitSha()
}
}
@@ -124,6 +124,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
//room
implementation 'androidx.room:room-runtime:2.0.0'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
kapt 'androidx.room:room-compiler:2.0.0'
implementation 'androidx.room:room-rxjava2:2.0.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
@@ -153,4 +154,7 @@ dependencies {
//Glide
implementation 'com.github.bumptech.glide:glide:4.9.0'
implementation 'com.github.bumptech.glide:okhttp3-integration:4.9.0'
+
+ //Add some useful extensions
+ implementation 'androidx.core:core-ktx:1.2.0-alpha01'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f08523f7f..07acb061a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -101,9 +101,7 @@
-
+
@@ -119,6 +117,8 @@
+
diff --git a/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java b/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java
deleted file mode 100644
index 319112122..000000000
--- a/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java
+++ /dev/null
@@ -1,215 +0,0 @@
-/* Copyright 2017 Andrew Dawson
- *
- * This file is a part of Tusky.
- *
- * This program is free software; you can redistribute it and/or modify it under the terms of the
- * GNU General Public License as published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
- * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
- * Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with Tusky; if not,
- * see . */
-
-package com.keylesspalace.tusky;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.EditText;
-
-import com.google.android.material.snackbar.Snackbar;
-import com.keylesspalace.tusky.adapter.ReportAdapter;
-import com.keylesspalace.tusky.di.Injectable;
-import com.keylesspalace.tusky.entity.Status;
-import com.keylesspalace.tusky.network.MastodonApi;
-import com.keylesspalace.tusky.util.HtmlUtils;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import javax.inject.Inject;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.widget.Toolbar;
-import androidx.recyclerview.widget.DividerItemDecoration;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import okhttp3.ResponseBody;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-public class ReportActivity extends BaseActivity implements Injectable {
- private static final String TAG = "ReportActivity"; // logging tag
-
- @Inject
- public MastodonApi mastodonApi;
-
- private View anyView; // what Snackbar will use to find the root view
- private ReportAdapter adapter;
- private boolean reportAlreadyInFlight;
- private String accountId;
- private EditText comment;
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_report);
-
- Intent intent = getIntent();
- accountId = intent.getStringExtra("account_id");
- String accountUsername = intent.getStringExtra("account_username");
- String statusId = intent.getStringExtra("status_id");
- String statusContent = intent.getStringExtra("status_content");
-
- Toolbar toolbar = findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
- ActionBar bar = getSupportActionBar();
- if (bar != null) {
- String title = String.format(getString(R.string.report_username_format),
- accountUsername);
- bar.setTitle(title);
- bar.setDisplayHomeAsUpEnabled(true);
- bar.setDisplayShowHomeEnabled(true);
- }
- anyView = toolbar;
-
- final RecyclerView recyclerView = findViewById(R.id.report_recycler_view);
- recyclerView.setHasFixedSize(true);
- LinearLayoutManager layoutManager = new LinearLayoutManager(this);
- recyclerView.setLayoutManager(layoutManager);
- adapter = new ReportAdapter();
- recyclerView.setAdapter(adapter);
-
- DividerItemDecoration divider = new DividerItemDecoration(
- this, layoutManager.getOrientation());
- recyclerView.addItemDecoration(divider);
-
- ReportAdapter.ReportStatus reportStatus = new ReportAdapter.ReportStatus(statusId,
- HtmlUtils.fromHtml(statusContent), true);
- adapter.addItem(reportStatus);
-
- comment = findViewById(R.id.report_comment);
-
- reportAlreadyInFlight = false;
-
- fetchRecentStatuses(accountId);
- }
-
- private void onClickSend() {
- if (reportAlreadyInFlight) {
- return;
- }
-
- String[] statusIds = adapter.getCheckedStatusIds();
-
- if (statusIds.length > 0) {
- reportAlreadyInFlight = true;
- sendReport(accountId, statusIds, comment.getText().toString());
- } else {
- comment.setError(getString(R.string.error_report_too_few_statuses));
- }
- }
-
- private void sendReport(final String accountId, final String[] statusIds,
- final String comment) {
- Callback callback = new Callback() {
- @Override
- public void onResponse(Call call, Response response) {
- if (response.isSuccessful()) {
- onSendSuccess();
- } else {
- onSendFailure(accountId, statusIds, comment);
- }
- }
-
- @Override
- public void onFailure(Call call, Throwable t) {
- onSendFailure(accountId, statusIds, comment);
- }
- };
- mastodonApi.report(accountId, Arrays.asList(statusIds), comment)
- .enqueue(callback);
- }
-
- private void onSendSuccess() {
- Snackbar bar = Snackbar.make(anyView, getString(R.string.confirmation_reported), Snackbar.LENGTH_SHORT);
- bar.show();
- finish();
- }
-
- private void onSendFailure(final String accountId, final String[] statusIds,
- final String comment) {
- Snackbar.make(anyView, R.string.error_generic, Snackbar.LENGTH_LONG)
- .setAction(R.string.action_retry, new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- sendReport(accountId, statusIds, comment);
- }
- })
- .show();
- reportAlreadyInFlight = false;
- }
-
- private void fetchRecentStatuses(String accountId) {
- Callback> callback = new Callback>() {
- @Override
- public void onResponse(Call> call, Response> response) {
- if (!response.isSuccessful()) {
- onFetchStatusesFailure(new Exception(response.message()));
- return;
- }
- List statusList = response.body();
- List itemList = new ArrayList<>();
- for (Status status : statusList) {
- if (status.getReblog() == null) {
- ReportAdapter.ReportStatus item = new ReportAdapter.ReportStatus(
- status.getId(), status.getContent(), false);
- itemList.add(item);
- }
- }
- adapter.addItems(itemList);
- }
-
- @Override
- public void onFailure(Call> call, Throwable t) {
- onFetchStatusesFailure((Exception) t);
- }
- };
- mastodonApi.accountStatuses(accountId, null, null, null, null, null, null)
- .enqueue(callback);
- }
-
- private void onFetchStatusesFailure(Exception exception) {
- Log.e(TAG, "Failed to fetch recent statuses to report. " + exception.getMessage());
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.report_toolbar, menu);
- return super.onCreateOptionsMenu(menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home: {
- onBackPressed();
- return true;
- }
- case R.id.action_report: {
- onClickSend();
- return true;
- }
- }
- return super.onOptionsItemSelected(item);
- }
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java
index d0ed9acfa..663dbc84f 100644
--- a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java
@@ -15,6 +15,8 @@
package com.keylesspalace.tusky;
+import android.content.Context;
+import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
@@ -34,16 +36,23 @@ import dagger.android.DispatchingAndroidInjector;
import dagger.android.support.HasSupportFragmentInjector;
public class ViewTagActivity extends BottomSheetActivity implements HasSupportFragmentInjector {
+ private static final String HASHTAG = "hashtag";
@Inject
public DispatchingAndroidInjector dispatchingAndroidInjector;
+ public static Intent getIntent(Context context, String tag){
+ Intent intent = new Intent(context,ViewTagActivity.class);
+ intent.putExtra(HASHTAG,tag);
+ return intent;
+ }
+
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_tag);
- String hashtag = getIntent().getStringExtra("hashtag");
+ String hashtag = getIntent().getStringExtra(HASHTAG);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ReportAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/ReportAdapter.java
deleted file mode 100644
index 0b0ac3fd9..000000000
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/ReportAdapter.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/* Copyright 2017 Andrew Dawson
- *
- * This file is a part of Tusky.
- *
- * This program is free software; you can redistribute it and/or modify it under the terms of the
- * GNU General Public License as published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
- * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
- * Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with Tusky; if not,
- * see . */
-
-package com.keylesspalace.tusky.adapter;
-
-import androidx.recyclerview.widget.RecyclerView;
-import android.text.Spanned;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import android.widget.TextView;
-
-import com.keylesspalace.tusky.R;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class ReportAdapter extends RecyclerView.Adapter {
- public static class ReportStatus {
- String id;
- Spanned content;
- boolean checked;
-
- public ReportStatus(String id, Spanned content, boolean checked) {
- this.id = id;
- this.content = content;
- this.checked = checked;
- }
-
- @Override
- public int hashCode() {
- return id.hashCode();
- }
-
- @Override
- public boolean equals(Object other) {
- if (this.id == null) {
- return this == other;
- } else if (!(other instanceof ReportStatus)) {
- return false;
- }
- ReportStatus status = (ReportStatus) other;
- return status.id.equals(this.id);
- }
- }
-
- private List statusList;
-
- public ReportAdapter() {
- super();
- statusList = new ArrayList<>();
- }
-
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View view = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.item_report_status, parent, false);
- return new ReportStatusViewHolder(view);
- }
-
- @Override
- public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
- ReportStatusViewHolder holder = (ReportStatusViewHolder) viewHolder;
- ReportStatus status = statusList.get(position);
- holder.setupWithStatus(status);
- }
-
- @Override
- public int getItemCount() {
- return statusList.size();
- }
-
- public void addItem(ReportStatus status) {
- int end = statusList.size();
- statusList.add(status);
- notifyItemInserted(end);
- }
-
- public void addItems(List newStatuses) {
- int end = statusList.size();
- int added = 0;
- for (ReportStatus status : newStatuses) {
- if (!statusList.contains(status)) {
- statusList.add(status);
- added += 1;
- }
- }
- if (added > 0) {
- notifyItemRangeInserted(end, added);
- }
- }
-
- public String[] getCheckedStatusIds() {
- List idList = new ArrayList<>();
- for (ReportStatus status : statusList) {
- if (status.checked) {
- idList.add(status.id);
- }
- }
- return idList.toArray(new String[0]);
- }
-
- private static class ReportStatusViewHolder extends RecyclerView.ViewHolder {
- private TextView content;
- private CheckBox checkBox;
-
- ReportStatusViewHolder(View view) {
- super(view);
- content = view.findViewById(R.id.report_status_content);
- checkBox = view.findViewById(R.id.report_status_check_box);
- }
-
- void setupWithStatus(final ReportStatus status) {
- content.setText(status.content);
- checkBox.setChecked(status.checked);
- checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- status.checked = isChecked;
- }
- });
- }
- }
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt
new file mode 100644
index 000000000..170c108a4
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt
@@ -0,0 +1,162 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.text.Spanned
+import android.view.MenuItem
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import com.keylesspalace.tusky.BottomSheetActivity
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter
+import com.keylesspalace.tusky.di.ViewModelFactory
+import com.keylesspalace.tusky.util.HtmlUtils
+import com.keylesspalace.tusky.util.ThemeUtils
+import dagger.android.AndroidInjector
+import dagger.android.DispatchingAndroidInjector
+import dagger.android.support.HasSupportFragmentInjector
+import kotlinx.android.synthetic.main.activity_report.*
+import kotlinx.android.synthetic.main.toolbar_basic.*
+import javax.inject.Inject
+
+
+class ReportActivity : BottomSheetActivity(), HasSupportFragmentInjector {
+
+ @Inject
+ lateinit var dispatchingFragmentInjector: DispatchingAndroidInjector
+
+ @Inject
+ lateinit var viewModelFactory: ViewModelFactory
+
+ private lateinit var viewModel: ReportViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ viewModel = ViewModelProviders.of(this, viewModelFactory)[ReportViewModel::class.java]
+ val accountId = intent?.getStringExtra(ACCOUNT_ID)
+ val accountUserName = intent?.getStringExtra(ACCOUNT_USERNAME)
+ if (accountId.isNullOrBlank() || accountUserName.isNullOrBlank()) {
+ throw IllegalStateException("accountId ($accountId) or accountUserName ($accountUserName) is null")
+ }
+
+ viewModel.init(accountId, accountUserName,
+ intent?.getStringExtra(STATUS_ID), intent?.getStringExtra(STATUS_CONTENT))
+
+
+ setContentView(R.layout.activity_report)
+
+ setSupportActionBar(toolbar)
+
+ val closeIcon = AppCompatResources.getDrawable(this, R.drawable.ic_close_24dp)
+ ThemeUtils.setDrawableTint(this, closeIcon!!, R.attr.compose_close_button_tint)
+
+ supportActionBar?.apply {
+ title = getString(R.string.report_username_format, viewModel.accountUserName)
+ setDisplayHomeAsUpEnabled(true)
+ setDisplayShowHomeEnabled(true)
+ setHomeAsUpIndicator(closeIcon)
+ }
+
+ initViewPager()
+ if (savedInstanceState == null) {
+ viewModel.navigateTo(Screen.Statuses)
+ }
+ subscribeObservables()
+ }
+
+ private fun initViewPager() {
+ wizard.adapter = ReportPagerAdapter(supportFragmentManager)
+ }
+
+ private fun subscribeObservables() {
+ viewModel.navigation.observe(this, Observer { screen ->
+ if (screen != null) {
+ viewModel.navigated()
+ when (screen) {
+ Screen.Statuses -> showStatusesPage()
+ Screen.Note -> showNotesPage()
+ Screen.Done -> showDonePage()
+ Screen.Back -> showPreviousScreen()
+ Screen.Finish -> closeScreen()
+ }
+ }
+ })
+
+ viewModel.checkUrl.observe(this, Observer {
+ if (!it.isNullOrBlank()) {
+ viewModel.urlChecked()
+ viewUrl(it)
+ }
+ })
+ }
+
+ private fun showPreviousScreen() {
+ when (wizard.currentItem) {
+ 0 -> closeScreen()
+ 1 -> showStatusesPage()
+ }
+ }
+
+ private fun showDonePage() {
+ wizard.currentItem = 2
+ }
+
+ private fun showNotesPage() {
+ wizard.currentItem = 1
+ }
+
+ private fun closeScreen() {
+ finish()
+ }
+
+ private fun showStatusesPage() {
+ wizard.currentItem = 0
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ android.R.id.home -> {
+ closeScreen()
+ return true
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ companion object {
+ private const val ACCOUNT_ID = "account_id"
+ private const val ACCOUNT_USERNAME = "account_username"
+ private const val STATUS_ID = "status_id"
+ private const val STATUS_CONTENT = "status_content"
+
+ @JvmStatic
+ fun getIntent(context: Context, accountId: String, userName: String, statusId: String, statusContent: Spanned) =
+ Intent(context, ReportActivity::class.java)
+ .apply {
+ putExtra(ACCOUNT_ID, accountId)
+ putExtra(ACCOUNT_USERNAME, userName)
+ putExtra(STATUS_ID, statusId)
+ putExtra(STATUS_CONTENT, HtmlUtils.toHtml(statusContent))
+ }
+ }
+
+ override fun supportFragmentInjector(): AndroidInjector = dispatchingFragmentInjector
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt
new file mode 100644
index 000000000..c772ff9b1
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt
@@ -0,0 +1,224 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
+import androidx.lifecycle.ViewModel
+import androidx.paging.PagedList
+import com.keylesspalace.tusky.components.report.adapter.StatusesRepository
+import com.keylesspalace.tusky.components.report.model.StatusViewState
+import com.keylesspalace.tusky.entity.Relationship
+import com.keylesspalace.tusky.entity.Status
+import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.util.*
+import io.reactivex.Single
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.schedulers.Schedulers
+import javax.inject.Inject
+
+class ReportViewModel @Inject constructor(
+ private val mastodonApi: MastodonApi,
+ private val statusesRepository: StatusesRepository) : ViewModel() {
+ private val disposables = CompositeDisposable()
+
+ private val navigationMutable = MutableLiveData()
+ val navigation: LiveData = navigationMutable
+
+ private val muteStateMutable = MutableLiveData>()
+ val muteState: LiveData> = muteStateMutable
+
+ private val blockStateMutable = MutableLiveData>()
+ val blockState: LiveData> = blockStateMutable
+
+ private val reportingStateMutable = MutableLiveData>()
+ var reportingState: LiveData> = reportingStateMutable
+
+ private val checkUrlMutable = MutableLiveData()
+ val checkUrl: LiveData = checkUrlMutable
+
+ private val repoResult = MutableLiveData>()
+ val statuses: LiveData> = Transformations.switchMap(repoResult) { it.pagedList }
+ val networkStateAfter: LiveData = Transformations.switchMap(repoResult) { it.networkStateAfter }
+ val networkStateBefore: LiveData = Transformations.switchMap(repoResult) { it.networkStateBefore }
+ val networkStateRefresh: LiveData = Transformations.switchMap(repoResult) { it.refreshState }
+
+ private val selectedIds = HashSet()
+ val statusViewState = StatusViewState()
+
+ var reportNote: String? = null
+ var isRemoteNotify = false
+
+ private var statusContent: String? = null
+ private var statusId: String? = null
+ lateinit var accountUserName: String
+ lateinit var accountId: String
+ var isRemoteAccount: Boolean = false
+ var remoteServer: String? = null
+
+ fun init(accountId: String, userName: String, statusId: String?, statusContent: String?) {
+ this.accountId = accountId
+ this.accountUserName = userName
+ this.statusId = statusId
+ statusId?.let {
+ selectedIds.add(it)
+ }
+ this.statusContent = statusContent
+ isRemoteAccount = userName.contains('@')
+ if (isRemoteAccount) {
+ remoteServer = userName.substring(userName.indexOf('@') + 1)
+ }
+
+ obtainRelationship()
+ repoResult.value = statusesRepository.getStatuses(accountId, statusId, disposables)
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ disposables.clear()
+ }
+
+ fun navigateTo(screen: Screen) {
+ navigationMutable.value = screen
+ }
+
+ fun navigated() {
+ navigationMutable.value = null
+ }
+
+
+ private fun obtainRelationship() {
+ val ids = listOf(accountId)
+ muteStateMutable.value = Loading()
+ blockStateMutable.value = Loading()
+ disposables.add(
+ mastodonApi.relationshipsObservable(ids)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ { data ->
+ updateRelationship(data.getOrNull(0))
+
+ },
+ {
+ updateRelationship(null)
+ }
+ ))
+ }
+
+
+ private fun updateRelationship(relationship: Relationship?) {
+ if (relationship != null) {
+ muteStateMutable.value = Success(relationship.muting)
+ blockStateMutable.value = Success(relationship.blocking)
+ } else {
+ muteStateMutable.value = Error(false)
+ blockStateMutable.value = Error(false)
+ }
+ }
+
+ fun toggleMute() {
+ val single: Single = if (muteStateMutable.value?.data == true) {
+ mastodonApi.unmuteAccountObservable(accountId)
+ } else {
+ mastodonApi.muteAccountObservable(accountId)
+ }
+ muteStateMutable.value = Loading()
+ disposables.add(
+ single
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ { relationship ->
+ muteStateMutable.value = Success(relationship?.muting == true)
+ },
+ { error ->
+ muteStateMutable.value = Error(false, error.message)
+ }
+ ))
+ }
+
+ fun toggleBlock() {
+ val single: Single = if (blockStateMutable.value?.data == true) {
+ mastodonApi.unblockAccountObservable(accountId)
+ } else {
+ mastodonApi.blockAccountObservable(accountId)
+ }
+ blockStateMutable.value = Loading()
+ disposables.add(
+ single
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ { relationship ->
+ blockStateMutable.value = Success(relationship?.blocking == true)
+ },
+ { error ->
+ blockStateMutable.value = Error(false, error.message)
+ }
+ ))
+ }
+
+ fun doReport() {
+ reportingStateMutable.value = Loading()
+ disposables.add(
+ mastodonApi.reportObservable(accountId, selectedIds.toList(), reportNote, if (isRemoteAccount) isRemoteNotify else null)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ reportingStateMutable.value = Success(true)
+ },
+ { error ->
+ reportingStateMutable.value = Error(cause = error)
+ }
+ )
+ )
+ }
+
+ fun retryStatusLoad() {
+ repoResult.value?.retry?.invoke()
+ }
+
+ fun refreshStatuses() {
+ repoResult.value?.refresh?.invoke()
+ }
+
+ fun checkClickedUrl(url: String?) {
+ checkUrlMutable.value = url
+ }
+
+ fun urlChecked() {
+ checkUrlMutable.value = null
+ }
+
+ fun setStatusChecked(status: Status, checked: Boolean) {
+ if (checked)
+ selectedIds.add(status.id)
+ else
+ selectedIds.remove(status.id)
+ }
+
+ fun isStatusChecked(id: String): Boolean {
+ return selectedIds.contains(id)
+ }
+
+ fun isStatusesSelected(): Boolean {
+ return selectedIds.isNotEmpty()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt
new file mode 100644
index 000000000..643c46c18
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt
@@ -0,0 +1,24 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report
+
+enum class Screen {
+ Statuses,
+ Note,
+ Done,
+ Back,
+ Finish
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt
new file mode 100644
index 000000000..588cfef0e
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt
@@ -0,0 +1,28 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report.adapter
+
+import android.view.View
+import com.keylesspalace.tusky.entity.Attachment
+import com.keylesspalace.tusky.entity.Status
+import com.keylesspalace.tusky.interfaces.LinkListener
+import java.util.ArrayList
+
+interface AdapterHandler: LinkListener {
+ fun showMedia(v: View?, status: Status?, idx: Int)
+ fun setStatusChecked(status: Status, isChecked: Boolean)
+ fun isStatusChecked(id: String): Boolean
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt
new file mode 100644
index 000000000..cf90fee1f
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt
@@ -0,0 +1,36 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report.adapter
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentPagerAdapter
+import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment
+import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment
+import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment
+
+class ReportPagerAdapter(manager: FragmentManager) : FragmentPagerAdapter(manager) {
+ override fun getItem(position: Int): Fragment {
+ return when (position) {
+ 0 -> ReportStatusesFragment.newInstance()
+ 1 -> ReportNoteFragment.newInstance()
+ 2 -> ReportDoneFragment.newInstance()
+ else -> throw IllegalArgumentException("Unknown page index: $position")
+ }
+ }
+
+ override fun getCount(): Int = 3
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt
new file mode 100644
index 000000000..a3d919501
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt
@@ -0,0 +1,166 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report.adapter
+
+import android.text.Spanned
+import android.text.TextUtils
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.components.report.model.StatusViewState
+import com.keylesspalace.tusky.entity.Emoji
+import com.keylesspalace.tusky.entity.Status
+import com.keylesspalace.tusky.interfaces.LinkListener
+import com.keylesspalace.tusky.util.*
+import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER
+import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER
+import kotlinx.android.synthetic.main.item_report_status.view.*
+import java.util.*
+
+class StatusViewHolder(itemView: View,
+ private val useAbsoluteTime: Boolean,
+ private val mediaPreviewEnabled: Boolean,
+ private val viewState: StatusViewState,
+ private val adapterHandler: AdapterHandler,
+ private val getStatusForPosition: (Int) -> Status?) : RecyclerView.ViewHolder(itemView) {
+ private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
+ private val statusViewHelper = StatusViewHelper(itemView)
+
+ private val previewListener = object : StatusViewHelper.MediaPreviewListener {
+ override fun onViewMedia(v: View?, idx: Int) {
+ status()?.let { status ->
+ adapterHandler.showMedia(v, status, idx)
+ }
+ }
+
+ override fun onContentHiddenChange(isShowing: Boolean) {
+ status()?.id?.let { id ->
+ viewState.setMediaShow(id, isShowing)
+ }
+ }
+ }
+
+ init {
+ itemView.statusSelection.setOnCheckedChangeListener { _, isChecked ->
+ status()?.let { status ->
+ adapterHandler.setStatusChecked(status, isChecked)
+ }
+ }
+ }
+
+ fun bind(status: Status) {
+ itemView.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id)
+
+ updateTextView()
+
+ val sensitive = status.sensitive
+
+ statusViewHelper.setMediasPreview(mediaPreviewEnabled, status.attachments, sensitive, previewListener,
+ viewState.isMediaShow(status.id, status.sensitive),
+ mediaViewHeight)
+
+ statusViewHelper.setupPollReadonly(status.poll, status.emojis, useAbsoluteTime)
+ setCreatedAt(status.createdAt)
+ }
+
+ private fun updateTextView() {
+ status()?.let { status ->
+ setupCollapsedState(status.isCollapsible(), viewState.isCollapsed(status.id, true),
+ viewState.isContentShow(status.id, status.sensitive), status.spoilerText)
+
+ if (status.spoilerText.isBlank()) {
+ setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler)
+ itemView.statusContentWarningButton.hide()
+ itemView.statusContentWarningDescription.hide()
+ } else {
+ val emojiSpoiler = CustomEmojiHelper.emojifyString(status.spoilerText, status.emojis, itemView.statusContentWarningDescription)
+ itemView.statusContentWarningDescription.text = emojiSpoiler
+ itemView.statusContentWarningDescription.show()
+ itemView.statusContentWarningButton.show()
+ itemView.statusContentWarningButton.isChecked = viewState.isContentShow(status.id, true)
+ itemView.statusContentWarningButton.setOnCheckedChangeListener { _, isViewChecked ->
+ status()?.let { status ->
+ itemView.statusContentWarningDescription.invalidate()
+ viewState.setContentShow(status.id, isViewChecked)
+ setTextVisible(isViewChecked, status.content, status.mentions, status.emojis, adapterHandler)
+ }
+ }
+ setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.emojis, adapterHandler)
+ }
+ }
+ }
+
+
+ private fun setTextVisible(expanded: Boolean,
+ content: Spanned,
+ mentions: Array?,
+ emojis: List,
+ listener: LinkListener) {
+ if (expanded) {
+ val emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, itemView.statusContent)
+ LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener)
+ } else {
+ LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener)
+ }
+ if (itemView.statusContent.text.isNullOrBlank()) {
+ itemView.statusContent.hide()
+ } else {
+ itemView.statusContent.show()
+ }
+ }
+
+ private fun setCreatedAt(createdAt: Date?) {
+ if (useAbsoluteTime) {
+ itemView.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt)
+ } else {
+ itemView.timestampInfo.text = if (createdAt != null) {
+ val then = createdAt.time
+ val now = System.currentTimeMillis()
+ DateUtils.getRelativeTimeSpanString(itemView.timestampInfo.context, then, now)
+ } else {
+ // unknown minutes~
+ "?m"
+ }
+ }
+ }
+
+
+ private fun setupCollapsedState(collapsible: Boolean, collapsed: Boolean, expanded: Boolean, spoilerText: String) {
+ /* input filter for TextViews have to be set before text */
+ if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
+ itemView.buttonToggleContent.setOnCheckedChangeListener { _, isChecked ->
+ status()?.let { status ->
+ viewState.setCollapsed(status.id, isChecked)
+ updateTextView()
+ }
+ }
+
+ itemView.buttonToggleContent.show()
+ if (collapsed) {
+ itemView.buttonToggleContent.isChecked = true
+ itemView.statusContent.filters = COLLAPSE_INPUT_FILTER
+ } else {
+ itemView.buttonToggleContent.isChecked = false
+ itemView.statusContent.filters = NO_INPUT_FILTER
+ }
+ } else {
+ itemView.buttonToggleContent.hide()
+ itemView.statusContent.filters = NO_INPUT_FILTER
+ }
+ }
+
+ private fun status() = getStatusForPosition(adapterPosition)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt
new file mode 100644
index 000000000..c5ecea09f
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt
@@ -0,0 +1,62 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report.adapter
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.paging.PagedListAdapter
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.components.report.model.StatusViewState
+import com.keylesspalace.tusky.entity.Status
+
+class StatusesAdapter(private val useAbsoluteTime: Boolean,
+ private val mediaPreviewEnabled: Boolean,
+ private val statusViewState: StatusViewState,
+ private val adapterHandler: AdapterHandler)
+ : PagedListAdapter(STATUS_COMPARATOR) {
+
+ private val statusForPosition: (Int) -> Status? = { position: Int ->
+ if (position != RecyclerView.NO_POSITION) getItem(position) else null
+ }
+
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ return StatusViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_report_status, parent, false),
+ useAbsoluteTime, mediaPreviewEnabled, statusViewState, adapterHandler, statusForPosition)
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ getItem(position)?.let { status ->
+ (holder as? StatusViewHolder)?.bind(status)
+ }
+
+ }
+
+ companion object {
+
+ val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() {
+ override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
+ oldItem == newItem
+
+ override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean =
+ oldItem.id == newItem.id
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt
new file mode 100644
index 000000000..8ecfbff21
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt
@@ -0,0 +1,145 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report.adapter
+
+import android.annotation.SuppressLint
+import androidx.lifecycle.MutableLiveData
+import androidx.paging.ItemKeyedDataSource
+import com.keylesspalace.tusky.entity.Status
+import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.util.NetworkState
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.functions.BiFunction
+import java.util.concurrent.Executor
+
+class StatusesDataSource(private val accountId: String,
+ private val mastodonApi: MastodonApi,
+ private val disposables: CompositeDisposable,
+ private val retryExecutor: Executor) : ItemKeyedDataSource() {
+
+ val networkStateAfter = MutableLiveData()
+ val networkStateBefore = MutableLiveData()
+
+ private var retryAfter: (() -> Any)? = null
+ private var retryBefore: (() -> Any)? = null
+ private var retryInitial: (() -> Any)? = null
+
+ val initialLoad = MutableLiveData()
+ fun retryAllFailed() {
+ var prevRetry = retryInitial
+ retryInitial = null
+ prevRetry?.let {
+ retryExecutor.execute {
+ it.invoke()
+ }
+ }
+
+ prevRetry = retryAfter
+ retryAfter = null
+ prevRetry?.let {
+ retryExecutor.execute {
+ it.invoke()
+ }
+ }
+
+ prevRetry = retryBefore
+ retryBefore = null
+ prevRetry?.let {
+ retryExecutor.execute {
+ it.invoke()
+ }
+ }
+ }
+
+ @SuppressLint("CheckResult")
+ override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) {
+ networkStateAfter.postValue(NetworkState.LOADED)
+ networkStateBefore.postValue(NetworkState.LOADED)
+ retryAfter = null
+ retryBefore = null
+ retryInitial = null
+ initialLoad.postValue(NetworkState.LOADING)
+ mastodonApi.statusObservable(params.requestedInitialKey).zipWith(
+ mastodonApi.accountStatusesObservable(accountId, params.requestedInitialKey, null, params.requestedLoadSize - 1, true),
+ BiFunction { status: Status, list: List ->
+ val ret = ArrayList()
+ ret.add(status)
+ ret.addAll(list)
+ return@BiFunction ret
+ })
+ .doOnSubscribe {
+ disposables.add(it)
+ }
+ .subscribe(
+ {
+ callback.onResult(it)
+ initialLoad.postValue(NetworkState.LOADED)
+ },
+ {
+ retryInitial = {
+ loadInitial(params, callback)
+ }
+ initialLoad.postValue(NetworkState.error(it.message))
+ }
+ )
+ }
+
+ @SuppressLint("CheckResult")
+ override fun loadAfter(params: LoadParams, callback: LoadCallback) {
+ networkStateAfter.postValue(NetworkState.LOADING)
+ retryAfter = null
+ mastodonApi.accountStatusesObservable(accountId, params.key, null, params.requestedLoadSize, true)
+ .doOnSubscribe {
+ disposables.add(it)
+ }
+ .subscribe(
+ {
+ callback.onResult(it)
+ networkStateAfter.postValue(NetworkState.LOADED)
+ },
+ {
+ retryAfter = {
+ loadAfter(params, callback)
+ }
+ networkStateAfter.postValue(NetworkState.error(it.message))
+ }
+ )
+ }
+
+ @SuppressLint("CheckResult")
+ override fun loadBefore(params: LoadParams, callback: LoadCallback) {
+ networkStateBefore.postValue(NetworkState.LOADING)
+ retryBefore = null
+ mastodonApi.accountStatusesObservable(accountId, null, params.key, params.requestedLoadSize, true)
+ .doOnSubscribe {
+ disposables.add(it)
+ }
+ .subscribe(
+ {
+ callback.onResult(it)
+ networkStateBefore.postValue(NetworkState.LOADED)
+ },
+ {
+ retryBefore = {
+ loadBefore(params, callback)
+ }
+ networkStateBefore.postValue(NetworkState.error(it.message))
+ }
+ )
+ }
+
+ override fun getKey(item: Status): String = item.id
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt
new file mode 100644
index 000000000..4cf8ff1c3
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt
@@ -0,0 +1,36 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report.adapter
+
+import androidx.lifecycle.MutableLiveData
+import androidx.paging.DataSource
+import com.keylesspalace.tusky.entity.Status
+import com.keylesspalace.tusky.network.MastodonApi
+import io.reactivex.disposables.CompositeDisposable
+import java.util.concurrent.Executor
+
+class StatusesDataSourceFactory(
+ private val accountId: String,
+ private val mastodonApi: MastodonApi,
+ private val disposables: CompositeDisposable,
+ private val retryExecutor: Executor) : DataSource.Factory() {
+ val sourceLiveData = MutableLiveData()
+ override fun create(): DataSource {
+ val source = StatusesDataSource(accountId, mastodonApi, disposables, retryExecutor)
+ sourceLiveData.postValue(source)
+ return source
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt
new file mode 100644
index 000000000..852a07f96
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt
@@ -0,0 +1,61 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report.adapter
+
+import androidx.lifecycle.Transformations
+import androidx.paging.Config
+import androidx.paging.toLiveData
+import com.keylesspalace.tusky.entity.Status
+import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.util.BiListing
+import com.keylesspalace.tusky.util.Listing
+import io.reactivex.disposables.CompositeDisposable
+import java.util.concurrent.Executors
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class StatusesRepository @Inject constructor(private val mastodonApi: MastodonApi) {
+
+ private val executor = Executors.newSingleThreadExecutor()
+
+ fun getStatuses(accountId: String, initialStatus: String?, disposables: CompositeDisposable, pageSize: Int = 20): BiListing {
+ val sourceFactory = StatusesDataSourceFactory(accountId, mastodonApi, disposables, executor)
+ val livePagedList = sourceFactory.toLiveData(
+ config = Config(pageSize = pageSize, enablePlaceholders = false, initialLoadSizeHint = pageSize * 2),
+ fetchExecutor = executor, initialLoadKey = initialStatus
+ )
+ return BiListing(
+ pagedList = livePagedList,
+ networkStateBefore = Transformations.switchMap(sourceFactory.sourceLiveData) {
+ it.networkStateBefore
+ },
+ networkStateAfter = Transformations.switchMap(sourceFactory.sourceLiveData) {
+ it.networkStateAfter
+ },
+ retry = {
+ sourceFactory.sourceLiveData.value?.retryAllFailed()
+ },
+ refresh = {
+ sourceFactory.sourceLiveData.value?.invalidate()
+ },
+ refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) {
+ it.initialLoad
+ }
+
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt
new file mode 100644
index 000000000..95bf73561
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt
@@ -0,0 +1,111 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report.fragments
+
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.components.report.ReportViewModel
+import com.keylesspalace.tusky.components.report.Screen
+import com.keylesspalace.tusky.di.Injectable
+import com.keylesspalace.tusky.di.ViewModelFactory
+import com.keylesspalace.tusky.util.Loading
+import com.keylesspalace.tusky.util.hide
+import com.keylesspalace.tusky.util.show
+import kotlinx.android.synthetic.main.fragment_report_done.*
+import javax.inject.Inject
+
+
+class ReportDoneFragment : Fragment(), Injectable {
+
+ @Inject
+ lateinit var viewModelFactory: ViewModelFactory
+
+ private lateinit var viewModel: ReportViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[ReportViewModel::class.java]
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.fragment_report_done, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName)
+ handleClicks()
+ subscribeObservables()
+ }
+
+ private fun subscribeObservables() {
+ viewModel.muteState.observe(viewLifecycleOwner, Observer {
+ if (it !is Loading) {
+ buttonMute.show()
+ progressMute.show()
+ } else {
+ buttonMute.hide()
+ progressMute.hide()
+ }
+
+ buttonMute.setText(when {
+ it.data == true -> R.string.action_unmute
+ else -> R.string.action_mute
+ })
+ })
+
+ viewModel.blockState.observe(viewLifecycleOwner, Observer {
+ if (it !is Loading) {
+ buttonBlock.show()
+ progressBlock.show()
+ }
+ else{
+ buttonBlock.hide()
+ progressBlock.hide()
+ }
+ buttonBlock.setText(when {
+ it.data == true -> R.string.action_unblock
+ else -> R.string.action_block
+ })
+ })
+
+ }
+
+ private fun handleClicks() {
+ buttonDone.setOnClickListener {
+ viewModel.navigateTo(Screen.Finish)
+ }
+ buttonBlock.setOnClickListener {
+ viewModel.toggleBlock()
+ }
+ buttonMute.setOnClickListener {
+ viewModel.toggleMute()
+ }
+ }
+
+ companion object {
+ fun newInstance() = ReportDoneFragment()
+ }
+
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt
new file mode 100644
index 000000000..216aa2575
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt
@@ -0,0 +1,141 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.widget.doAfterTextChanged
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import com.google.android.material.snackbar.Snackbar
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.components.report.ReportViewModel
+import com.keylesspalace.tusky.components.report.Screen
+import com.keylesspalace.tusky.di.Injectable
+import com.keylesspalace.tusky.di.ViewModelFactory
+import com.keylesspalace.tusky.util.*
+import kotlinx.android.synthetic.main.fragment_report_note.*
+import java.io.IOException
+import javax.inject.Inject
+
+class ReportNoteFragment : Fragment(), Injectable {
+
+ @Inject
+ lateinit var viewModelFactory: ViewModelFactory
+
+ private lateinit var viewModel: ReportViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[ReportViewModel::class.java]
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.fragment_report_note, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ fillViews()
+ handleChanges()
+ handleClicks()
+ subscribeObservables()
+ }
+
+ private fun handleChanges() {
+ editNote.doAfterTextChanged {
+ viewModel.reportNote = it?.toString()
+ }
+ checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked ->
+ viewModel.isRemoteNotify = isChecked
+ }
+ }
+
+ private fun fillViews() {
+ editNote.setText(viewModel.reportNote)
+
+ if (viewModel.isRemoteAccount){
+ checkIsNotifyRemote.show()
+ reportDescriptionRemoteInstance.show()
+ }
+ else{
+ checkIsNotifyRemote.hide()
+ reportDescriptionRemoteInstance.hide()
+ }
+
+ if (viewModel.isRemoteAccount)
+ checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer)
+ checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify
+ }
+
+ private fun subscribeObservables() {
+ viewModel.reportingState.observe(viewLifecycleOwner, Observer {
+ when (it) {
+ is Success -> viewModel.navigateTo(Screen.Done)
+ is Loading -> showLoading()
+ is Error -> showError(it.cause)
+
+ }
+ })
+ }
+
+ private fun showError(error: Throwable?) {
+ editNote.isEnabled = true
+ checkIsNotifyRemote.isEnabled = true
+ buttonReport.isEnabled = true
+ buttonBack.isEnabled = true
+ progressBar.hide()
+
+ Snackbar.make(buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG)
+ .apply {
+ setAction(R.string.action_retry) {
+ sendReport()
+ }
+ }
+ .show()
+ }
+
+ private fun sendReport() {
+ viewModel.doReport()
+ }
+
+ private fun showLoading() {
+ buttonReport.isEnabled = false
+ buttonBack.isEnabled = false
+ editNote.isEnabled = false
+ checkIsNotifyRemote.isEnabled = false
+ progressBar.show()
+ }
+
+ private fun handleClicks() {
+ buttonBack.setOnClickListener {
+ viewModel.navigateTo(Screen.Back)
+ }
+
+ buttonReport.setOnClickListener {
+ sendReport()
+ }
+ }
+
+ companion object {
+ fun newInstance() = ReportNoteFragment()
+ }
+
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt
new file mode 100644
index 000000000..13edcb44c
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt
@@ -0,0 +1,216 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report.fragments
+
+import android.os.Bundle
+import android.preference.PreferenceManager
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.app.ActivityOptionsCompat
+import androidx.core.view.ViewCompat
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import androidx.paging.PagedList
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.SimpleItemAnimator
+import com.google.android.material.snackbar.Snackbar
+import com.keylesspalace.tusky.AccountActivity
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.ViewMediaActivity
+import com.keylesspalace.tusky.ViewTagActivity
+import com.keylesspalace.tusky.components.report.ReportViewModel
+import com.keylesspalace.tusky.components.report.Screen
+import com.keylesspalace.tusky.components.report.adapter.AdapterHandler
+import com.keylesspalace.tusky.components.report.adapter.StatusesAdapter
+import com.keylesspalace.tusky.db.AccountManager
+import com.keylesspalace.tusky.di.Injectable
+import com.keylesspalace.tusky.di.ViewModelFactory
+import com.keylesspalace.tusky.entity.Attachment
+import com.keylesspalace.tusky.entity.Status
+import com.keylesspalace.tusky.util.ThemeUtils
+import com.keylesspalace.tusky.util.hide
+import com.keylesspalace.tusky.util.show
+import com.keylesspalace.tusky.viewdata.AttachmentViewData
+import kotlinx.android.synthetic.main.fragment_report_statuses.*
+import javax.inject.Inject
+
+class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
+
+ @Inject
+ lateinit var viewModelFactory: ViewModelFactory
+
+ @Inject
+ lateinit var accountManager: AccountManager
+
+ private lateinit var viewModel: ReportViewModel
+
+ private lateinit var adapter: StatusesAdapter
+ private lateinit var layoutManager: LinearLayoutManager
+
+ private var snackbarErrorRetry: Snackbar? = null
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[ReportViewModel::class.java]
+ }
+
+ override fun showMedia(v: View?, status: Status?, idx: Int) {
+ status?.actionableStatus?.let { actionable ->
+ when (actionable.attachments[idx].type) {
+ Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE -> {
+ val attachments = AttachmentViewData.list(actionable)
+ val intent = ViewMediaActivity.newIntent(context, attachments,
+ idx)
+ if (v != null) {
+ val url = actionable.attachments[idx].url
+ ViewCompat.setTransitionName(v, url)
+ val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(),
+ v, url)
+ startActivity(intent, options.toBundle())
+ } else {
+ startActivity(intent)
+ }
+ }
+ Attachment.Type.UNKNOWN -> {
+ }
+ }
+
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.fragment_report_statuses, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ handleClicks()
+ initStatusesView()
+ setupSwipeRefreshLayout()
+ }
+
+ private fun setupSwipeRefreshLayout() {
+ swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
+ swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(swipeRefreshLayout.context, android.R.attr.colorBackground))
+
+ swipeRefreshLayout.setOnRefreshListener {
+ snackbarErrorRetry?.dismiss()
+ viewModel.refreshStatuses()
+ }
+ }
+
+ private fun initStatusesView() {
+ val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
+
+ val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
+
+ val account = accountManager.activeAccount
+ val mediaPreviewEnabled = account?.mediaPreviewEnabled ?: true
+
+
+ adapter = StatusesAdapter(useAbsoluteTime, mediaPreviewEnabled, viewModel.statusViewState, this)
+
+ recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
+ layoutManager = LinearLayoutManager(requireContext())
+ recyclerView.layoutManager = layoutManager
+ recyclerView.adapter = adapter
+ (recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
+
+ viewModel.statuses.observe(viewLifecycleOwner, Observer> {
+ adapter.submitList(it)
+ })
+
+ viewModel.networkStateAfter.observe(viewLifecycleOwner, Observer {
+ if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
+ progressBarBottom.show()
+ else
+ progressBarBottom.hide()
+
+ if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
+ showError(it.msg)
+ })
+
+ viewModel.networkStateBefore.observe(viewLifecycleOwner, Observer {
+ if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
+ progressBarTop.show()
+ else
+ progressBarTop.hide()
+
+ if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
+ showError(it.msg)
+ })
+
+ viewModel.networkStateRefresh.observe(viewLifecycleOwner, Observer {
+ if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !swipeRefreshLayout.isRefreshing)
+ progressBarLoading.show()
+ else
+ progressBarLoading.hide()
+
+ if (it?.status != com.keylesspalace.tusky.util.Status.RUNNING)
+ swipeRefreshLayout.isRefreshing = false
+ if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
+ showError(it.msg)
+ })
+ }
+
+ private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) {
+ if (snackbarErrorRetry?.isShown != true) {
+ snackbarErrorRetry = Snackbar.make(swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE)
+ snackbarErrorRetry?.setAction(R.string.action_retry) {
+ viewModel.retryStatusLoad()
+ }
+ snackbarErrorRetry?.show()
+ }
+ }
+
+
+ private fun handleClicks() {
+ buttonCancel.setOnClickListener {
+ viewModel.navigateTo(Screen.Back)
+ }
+
+ buttonContinue.setOnClickListener {
+ if (viewModel.isStatusesSelected()) {
+ viewModel.navigateTo(Screen.Note)
+ } else {
+ Snackbar.make(swipeRefreshLayout, R.string.error_report_too_few_statuses, Snackbar.LENGTH_LONG).show()
+ }
+ }
+ }
+
+ override fun setStatusChecked(status: Status, isChecked: Boolean) {
+ viewModel.setStatusChecked(status, isChecked)
+ }
+
+ override fun isStatusChecked(id: String): Boolean {
+ return viewModel.isStatusChecked(id)
+ }
+
+ override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id))
+
+ override fun onViewTag(tag: String) = startActivity(ViewTagActivity.getIntent(requireContext(), tag))
+
+ override fun onViewUrl(url: String?) = viewModel.checkClickedUrl(url)
+
+ companion object {
+ fun newInstance() = ReportStatusesFragment()
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt
new file mode 100644
index 000000000..664ddc6a5
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt
@@ -0,0 +1,36 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.report.model
+
+class StatusViewState {
+ private val mediaShownState = HashMap()
+ private val contentShownState = HashMap()
+ private val longContentCollapsedState = HashMap()
+
+ fun isMediaShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(mediaShownState, id, !isSensitive)
+ fun setMediaShow(id: String, isShow: Boolean) = setStateEnabled(mediaShownState, id, isShow)
+
+ fun isContentShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(contentShownState, id, !isSensitive)
+ fun setContentShow(id: String, isShow: Boolean) = setStateEnabled(contentShownState, id, isShow)
+
+ fun isCollapsed(id: String, isCollapsed: Boolean): Boolean = isStateEnabled(longContentCollapsedState, id, isCollapsed)
+ fun setCollapsed(id: String, isCollapsed: Boolean) = setStateEnabled(longContentCollapsedState, id, isCollapsed)
+
+ private fun isStateEnabled(map: Map, id: String, def: Boolean): Boolean = map[id]
+ ?: def
+
+ private fun setStateEnabled(map: MutableMap, id: String, state: Boolean) = map.put(id, state)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
index 9ad5e0863..f516ae16e 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
@@ -16,6 +16,7 @@
package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.*
+import com.keylesspalace.tusky.components.report.ReportActivity
import dagger.Module
import dagger.android.ContributesAndroidInjector
@@ -71,9 +72,6 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesSplashActivity(): SplashActivity
- @ContributesAndroidInjector
- abstract fun contributesReportActivity(): ReportActivity
-
@ContributesAndroidInjector
abstract fun contributesSavedTootActivity(): SavedTootActivity
@@ -92,4 +90,6 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesFiltersActivity(): FiltersActivity
+ @ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
+ abstract fun contributesReportActivity(): ReportActivity
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
index 128bb1f5e..05f618a91 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
@@ -21,6 +21,9 @@ import com.keylesspalace.tusky.components.conversation.ConversationsFragment
import com.keylesspalace.tusky.fragment.*
import com.keylesspalace.tusky.fragment.preference.AccountPreferencesFragment
import com.keylesspalace.tusky.fragment.preference.NotificationPreferencesFragment
+import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment
+import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment
+import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@@ -60,4 +63,12 @@ abstract class FragmentBuildersModule {
@ContributesAndroidInjector
abstract fun accountInListsFragment(): AccountsInListFragment
+ @ContributesAndroidInjector
+ abstract fun reportStatusesFragment(): ReportStatusesFragment
+
+ @ContributesAndroidInjector
+ abstract fun reportNoteFragment(): ReportNoteFragment
+
+ @ContributesAndroidInjector
+ abstract fun reportDoneFragment(): ReportDoneFragment
}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
index 175c5b363..2a3605a51 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
@@ -4,10 +4,9 @@ package com.keylesspalace.tusky.di
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
-import com.keylesspalace.tusky.viewmodel.AccountViewModel
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
-import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
-import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
+import com.keylesspalace.tusky.components.report.ReportViewModel
+import com.keylesspalace.tusky.viewmodel.*
import com.keylesspalace.tusky.viewmodel.ListsViewModel
import dagger.Binds
import dagger.MapKey
@@ -61,5 +60,10 @@ abstract class ViewModelModule {
@ViewModelKey(AccountsInListViewModel::class)
internal abstract fun accountsInListViewModel(viewModel: AccountsInListViewModel): ViewModel
+ @Binds
+ @IntoMap
+ @ViewModelKey(ReportViewModel::class)
+ internal abstract fun reportViewModel(viewModel: ReportViewModel): ViewModel
+
//Add more ViewModels here
}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
index a435e32fc..833a1e51d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
@@ -44,9 +44,9 @@ import com.keylesspalace.tusky.BottomSheetActivity;
import com.keylesspalace.tusky.ComposeActivity;
import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.R;
-import com.keylesspalace.tusky.ReportActivity;
import com.keylesspalace.tusky.ViewMediaActivity;
import com.keylesspalace.tusky.ViewTagActivity;
+import com.keylesspalace.tusky.components.report.ReportActivity;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable;
@@ -54,7 +54,6 @@ import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases;
-import com.keylesspalace.tusky.util.HtmlUtils;
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
import java.util.LinkedHashSet;
@@ -327,12 +326,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
protected void openReportPage(String accountId, String accountUsername, String statusId,
Spanned statusContent) {
- Intent intent = new Intent(getContext(), ReportActivity.class);
- intent.putExtra("account_id", accountId);
- intent.putExtra("account_username", accountUsername);
- intent.putExtra("status_id", statusId);
- intent.putExtra("status_content", HtmlUtils.toHtml(statusContent));
- startActivity(intent);
+ startActivity(ReportActivity.getIntent(requireContext(),accountId,accountUsername,statusId,statusContent));
}
protected void showConfirmDeleteDialog(final String id, final int position) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java
index 9729d5d4e..c429e2727 100644
--- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java
+++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java
@@ -385,4 +385,40 @@ public interface MastodonApi {
@Path("id") String id,
@Field("choices[]") List choices
);
+
+ @POST("api/v1/accounts/{id}/block")
+ Single blockAccountObservable(@Path("id") String accountId);
+
+ @POST("api/v1/accounts/{id}/unblock")
+ Single unblockAccountObservable(@Path("id") String accountId);
+
+ @POST("api/v1/accounts/{id}/mute")
+ Single muteAccountObservable(@Path("id") String accountId);
+
+ @POST("api/v1/accounts/{id}/unmute")
+ Single unmuteAccountObservable(@Path("id") String accountId);
+
+ @GET("api/v1/accounts/relationships")
+ Single> relationshipsObservable(@Query("id[]") List accountIds);
+
+ @FormUrlEncoded
+ @POST("api/v1/reports")
+ Single reportObservable(
+ @Field("account_id") String accountId,
+ @Field("status_ids[]") List statusIds,
+ @Field("comment") String comment,
+ @Field("forward") Boolean isNotifyRemote);
+
+ @GET("api/v1/accounts/{id}/statuses")
+ Single> accountStatusesObservable(
+ @Path("id") String accountId,
+ @Query("max_id") String maxId,
+ @Query("since_id") String sinceId,
+ @Query("limit") Integer limit,
+ @Nullable @Query("exclude_reblogs") Boolean excludeReblogs);
+
+
+ @GET("api/v1/statuses/{id}")
+ Single statusObservable(@Path("id") String statusId);
+
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt b/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt
new file mode 100644
index 000000000..dad6d552d
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.keylesspalace.tusky.util
+
+import androidx.lifecycle.LiveData
+import androidx.paging.PagedList
+
+/**
+ * Data class that is necessary for a UI to show a listing and interact w/ the rest of the system
+ */
+data class BiListing(
+ // the LiveData of paged lists for the UI to observe
+ val pagedList: LiveData>,
+ // represents the network request status for load data before first to show to the user
+ val networkStateBefore: LiveData,
+ // represents the network request status for load data after last to show to the user
+ val networkStateAfter: LiveData,
+ // represents the refresh status to show to the user. Separate from networkState, this
+ // value is importantly only when refresh is requested.
+ val refreshState: LiveData,
+ // refreshes the whole data and fetches it from scratch.
+ val refresh: () -> Unit,
+ // retries any failed requests.
+ val retry: () -> Unit)
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt b/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt
index d6117c9b3..1f9f35d20 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt
@@ -8,5 +8,6 @@ class Success (override val data: T? = null) : Resource(data)
class Error (override val data: T? = null,
val errorMessage: String? = null,
- var consumed: Boolean = false
+ var consumed: Boolean = false,
+ val cause: Throwable? = null
): Resource(data)
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusExtensions.kt
new file mode 100644
index 000000000..e0cc7b492
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusExtensions.kt
@@ -0,0 +1,23 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.util
+
+import com.keylesspalace.tusky.entity.Status
+
+fun Status.isCollapsible(): Boolean {
+ return !SmartLengthInputFilter.hasBadRatio(content, SmartLengthInputFilter.LENGTH_DEFAULT)
+}
+
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt
new file mode 100644
index 000000000..931f1a4b1
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt
@@ -0,0 +1,314 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.util
+
+import android.content.Context
+import android.text.InputFilter
+import android.text.TextUtils
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.DrawableRes
+import androidx.appcompat.content.res.AppCompatResources
+import com.bumptech.glide.Glide
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.entity.Attachment
+import com.keylesspalace.tusky.entity.Emoji
+import com.keylesspalace.tusky.entity.Poll
+import com.keylesspalace.tusky.entity.Status
+import com.keylesspalace.tusky.view.MediaPreviewImageView
+import java.text.NumberFormat
+import java.text.SimpleDateFormat
+import java.util.*
+
+class StatusViewHelper(private val itemView: View) {
+ interface MediaPreviewListener {
+ fun onViewMedia(v: View?, idx: Int)
+ fun onContentHiddenChange(isShowing: Boolean)
+ }
+
+ private val shortSdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
+ private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault())
+
+ fun setMediasPreview(
+ mediaPreviewEnabled: Boolean,
+ attachments: List,
+ sensitive: Boolean,
+ previewListener: MediaPreviewListener,
+ showingContent: Boolean,
+ mediaPreviewHeight: Int) {
+
+ val context = itemView.context
+ val mediaPreviews = arrayOf(
+ itemView.findViewById(R.id.status_media_preview_0),
+ itemView.findViewById(R.id.status_media_preview_1),
+ itemView.findViewById(R.id.status_media_preview_2),
+ itemView.findViewById(R.id.status_media_preview_3))
+
+ val mediaOverlays = arrayOf(
+ itemView.findViewById(R.id.status_media_overlay_0),
+ itemView.findViewById(R.id.status_media_overlay_1),
+ itemView.findViewById(R.id.status_media_overlay_2),
+ itemView.findViewById(R.id.status_media_overlay_3))
+
+ val sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning)
+ val sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button)
+ val mediaLabel = itemView.findViewById(R.id.status_media_label)
+ if (mediaPreviewEnabled) {
+ // Hide the unused label.
+ mediaLabel.visibility = View.GONE
+ } else {
+ setMediaLabel(mediaLabel, attachments, sensitive, previewListener)
+ // Hide all unused views.
+ mediaPreviews[0].visibility = View.GONE
+ mediaPreviews[1].visibility = View.GONE
+ mediaPreviews[2].visibility = View.GONE
+ mediaPreviews[3].visibility = View.GONE
+ sensitiveMediaWarning.visibility = View.GONE
+ sensitiveMediaShow.visibility = View.GONE
+ return
+ }
+
+
+ val mediaPreviewUnloadedId = ThemeUtils.getDrawableId(context, R.attr.media_preview_unloaded_drawable, android.R.color.black)
+
+ val n = Math.min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS)
+
+ for (i in 0 until n) {
+ val previewUrl = attachments[i].previewUrl
+ val description = attachments[i].description
+
+ if (TextUtils.isEmpty(description)) {
+ mediaPreviews[i].contentDescription = context.getString(R.string.action_view_media)
+ } else {
+ mediaPreviews[i].contentDescription = description
+ }
+
+ mediaPreviews[i].visibility = View.VISIBLE
+
+ if (TextUtils.isEmpty(previewUrl)) {
+ Glide.with(mediaPreviews[i])
+ .load(mediaPreviewUnloadedId)
+ .centerInside()
+ .into(mediaPreviews[i])
+ } else {
+ val meta = attachments[i].meta
+ val focus = meta?.focus
+
+ if (focus != null) { // If there is a focal point for this attachment:
+ mediaPreviews[i].setFocalPoint(focus)
+
+ Glide.with(mediaPreviews[i])
+ .load(previewUrl)
+ .placeholder(mediaPreviewUnloadedId)
+ .centerInside()
+ .addListener(mediaPreviews[i])
+ .into(mediaPreviews[i])
+ } else {
+ mediaPreviews[i].removeFocalPoint()
+
+ Glide.with(mediaPreviews[i])
+ .load(previewUrl)
+ .placeholder(mediaPreviewUnloadedId)
+ .centerInside()
+ .into(mediaPreviews[i])
+ }
+ }
+
+ val type = attachments[i].type
+ if ((type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV)) {
+ mediaOverlays[i].visibility = View.VISIBLE
+ } else {
+ mediaOverlays[i].visibility = View.GONE
+ }
+
+ mediaPreviews[i].setOnClickListener { v ->
+ previewListener.onViewMedia(v, i)
+ }
+
+ if (n <= 2) {
+ mediaPreviews[0].layoutParams.height = mediaPreviewHeight * 2
+ mediaPreviews[1].layoutParams.height = mediaPreviewHeight * 2
+ } else {
+ mediaPreviews[0].layoutParams.height = mediaPreviewHeight
+ mediaPreviews[1].layoutParams.height = mediaPreviewHeight
+ mediaPreviews[2].layoutParams.height = mediaPreviewHeight
+ mediaPreviews[3].layoutParams.height = mediaPreviewHeight
+ }
+ }
+ if (attachments.isNullOrEmpty()) {
+ sensitiveMediaWarning.visibility = View.GONE
+ sensitiveMediaShow.visibility = View.GONE
+ } else {
+
+ val hiddenContentText: String = if (sensitive) {
+ context.getString(R.string.status_sensitive_media_template,
+ context.getString(R.string.status_sensitive_media_title),
+ context.getString(R.string.status_sensitive_media_directions))
+ } else {
+ context.getString(R.string.status_sensitive_media_template,
+ context.getString(R.string.status_media_hidden_title),
+ context.getString(R.string.status_sensitive_media_directions))
+ }
+
+ sensitiveMediaWarning.text = HtmlUtils.fromHtml(hiddenContentText)
+
+ sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE
+ sensitiveMediaShow.visibility = if (showingContent) View.VISIBLE else View.GONE
+ sensitiveMediaShow.setOnClickListener { v ->
+ previewListener.onContentHiddenChange(false)
+ v.visibility = View.GONE
+ sensitiveMediaWarning.visibility = View.VISIBLE
+ }
+ sensitiveMediaWarning.setOnClickListener { v ->
+ previewListener.onContentHiddenChange(true)
+ v.visibility = View.GONE
+ sensitiveMediaShow.visibility = View.VISIBLE
+ }
+ }
+
+ // Hide any of the placeholder previews beyond the ones set.
+ for (i in n until Status.MAX_MEDIA_ATTACHMENTS) {
+ mediaPreviews[i].visibility = View.GONE
+ }
+ }
+
+ private fun setMediaLabel(mediaLabel: TextView, attachments: List, sensitive: Boolean,
+ listener: MediaPreviewListener) {
+ if (attachments.isEmpty()) {
+ mediaLabel.visibility = View.GONE
+ return
+ }
+ mediaLabel.visibility = View.VISIBLE
+
+ // Set the label's text.
+ val context = mediaLabel.context
+ var labelText = getLabelTypeText(context, attachments[0].type)
+ if (sensitive) {
+ val sensitiveText = context.getString(R.string.status_sensitive_media_title)
+ labelText += String.format(" (%s)", sensitiveText)
+ }
+ mediaLabel.text = labelText
+
+ // Set the icon next to the label.
+ val drawableId = getLabelIcon(attachments[0].type)
+ val drawable = AppCompatResources.getDrawable(context, drawableId)
+ ThemeUtils.setDrawableTint(context, drawable!!, android.R.attr.textColorTertiary)
+ mediaLabel.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
+
+ mediaLabel.setOnClickListener { listener.onViewMedia(null, 0) }
+ }
+
+ private fun getLabelTypeText(context: Context, type: Attachment.Type): String {
+ return when (type) {
+ Attachment.Type.IMAGE -> context.getString(R.string.status_media_images)
+ Attachment.Type.GIFV, Attachment.Type.VIDEO -> context.getString(R.string.status_media_video)
+ else -> context.getString(R.string.status_media_images)
+ }
+ }
+
+ @DrawableRes
+ private fun getLabelIcon(type: Attachment.Type): Int {
+ return when (type) {
+ Attachment.Type.IMAGE -> R.drawable.ic_photo_24dp
+ Attachment.Type.GIFV, Attachment.Type.VIDEO -> R.drawable.ic_videocam_24dp
+ else -> R.drawable.ic_photo_24dp
+ }
+ }
+
+ fun setupPollReadonly(poll: Poll?, emojis: List, useAbsoluteTime: Boolean) {
+ val pollResults = listOf(
+ itemView.findViewById(R.id.status_poll_option_result_0),
+ itemView.findViewById(R.id.status_poll_option_result_1),
+ itemView.findViewById(R.id.status_poll_option_result_2),
+ itemView.findViewById(R.id.status_poll_option_result_3))
+
+ val pollDescription = itemView.findViewById(R.id.status_poll_description)
+
+ if (poll == null) {
+ for (pollResult in pollResults) {
+ pollResult.visibility = View.GONE
+ }
+ pollDescription.visibility = View.GONE
+ } else {
+ val timestamp = System.currentTimeMillis()
+
+
+ setupPollResult(poll, emojis, pollResults)
+
+ pollDescription.visibility = View.VISIBLE
+ pollDescription.text = getPollInfoText(timestamp, poll, pollDescription, useAbsoluteTime)
+ }
+ }
+
+ private fun getPollInfoText(timestamp: Long, poll: Poll, pollDescription: TextView, useAbsoluteTime: Boolean): CharSequence {
+ val context = pollDescription.context
+ val votes = NumberFormat.getNumberInstance().format(poll.votesCount.toLong())
+ val votesText = context.resources.getQuantityString(R.plurals.poll_info_votes, poll.votesCount, votes)
+ val pollDurationInfo: CharSequence
+ if (poll.expired) {
+ pollDurationInfo = context.getString(R.string.poll_info_closed)
+ } else {
+ if (useAbsoluteTime) {
+ pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.expiresAt))
+ } else {
+ val pollDuration = DateUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp)
+ pollDurationInfo = context.getString(R.string.poll_info_time_relative, pollDuration)
+ }
+ }
+
+ return context.getString(R.string.poll_info_format, votesText, pollDurationInfo)
+ }
+
+
+ private fun setupPollResult(poll: Poll, emojis: List, pollResults: List) {
+ val options = poll.options
+
+ for (i in 0 until Status.MAX_POLL_OPTIONS) {
+ if (i < options.size) {
+ val percent = options[i].getPercent(poll.votesCount)
+
+ val pollOptionText = pollResults[i].context.getString(R.string.poll_option_format, percent, options[i].title)
+ pollResults[i].text = CustomEmojiHelper.emojifyText(HtmlUtils.fromHtml(pollOptionText), emojis, pollResults[i])
+ pollResults[i].visibility = View.VISIBLE
+
+ val level = percent * 100
+
+ pollResults[i].background.level = level
+
+ } else {
+ pollResults[i].visibility = View.GONE
+ }
+ }
+ }
+
+ fun getAbsoluteTime(time: Date?): String {
+ return if (time != null) {
+ if (android.text.format.DateUtils.isToday(time.time)) {
+ shortSdf.format(time)
+ } else {
+ longSdf.format(time)
+ }
+ } else {
+ "??:??:??"
+ }
+ }
+
+ companion object {
+ val COLLAPSE_INPUT_FILTER = arrayOf(SmartLengthInputFilter.INSTANCE)
+ val NO_INPUT_FILTER = arrayOfNulls(0)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/view/NoSwipeViewPager.kt b/app/src/main/java/com/keylesspalace/tusky/view/NoSwipeViewPager.kt
new file mode 100644
index 000000000..8e4709125
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/view/NoSwipeViewPager.kt
@@ -0,0 +1,33 @@
+/* Copyright 2019 Joel Pyska
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import androidx.viewpager.widget.ViewPager
+
+class NoSwipeViewPager @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ViewPager(context, attrs) {
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
+ return false
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ return false
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/report_success_background.xml b/app/src/main/res/drawable/report_success_background.xml
new file mode 100644
index 000000000..147e04857
--- /dev/null
+++ b/app/src/main/res/drawable/report_success_background.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-land/fragment_report_done.xml b/app/src/main/res/layout-land/fragment_report_done.xml
new file mode 100644
index 000000000..d9900a2a8
--- /dev/null
+++ b/app/src/main/res/layout-land/fragment_report_done.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_report.xml b/app/src/main/res/layout/activity_report.xml
index 82591bdce..28c40d52f 100644
--- a/app/src/main/res/layout/activity_report.xml
+++ b/app/src/main/res/layout/activity_report.xml
@@ -1,40 +1,21 @@
+ tools:context=".components.report.ReportActivity">
-
+ android:overScrollMode="never"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
-
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_report_done.xml b/app/src/main/res/layout/fragment_report_done.xml
new file mode 100644
index 000000000..fa5e0e5d9
--- /dev/null
+++ b/app/src/main/res/layout/fragment_report_done.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_report_note.xml b/app/src/main/res/layout/fragment_report_note.xml
new file mode 100644
index 000000000..2eab8b2c3
--- /dev/null
+++ b/app/src/main/res/layout/fragment_report_note.xml
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_report_statuses.xml b/app/src/main/res/layout/fragment_report_statuses.xml
new file mode 100644
index 000000000..12f50bdf4
--- /dev/null
+++ b/app/src/main/res/layout/fragment_report_statuses.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_report_status.xml b/app/src/main/res/layout/item_report_status.xml
index 5c5f58999..4aba26583 100644
--- a/app/src/main/res/layout/item_report_status.xml
+++ b/app/src/main/res/layout/item_report_status.xml
@@ -1,22 +1,357 @@
-
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp">
-
+
+
+
+
+
+
+ android:textColor="?android:textColorPrimary"
+ android:textSize="?attr/status_text_medium"
+ app:layout_constraintEnd_toStartOf="@id/barrierEnd"
+ app:layout_constraintStart_toStartOf="@id/guideBegin"
+ app:layout_constraintTop_toBottomOf="@id/statusContentWarningButton" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_margin="16dp"
+ app:buttonTint="?attr/compound_button_color"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/timestampInfo" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 800df7d42..8fc2da67f 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -46,5 +46,4 @@
-
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 81f7896bb..a58bf369d 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -39,4 +39,5 @@
5.25dp4.5dp3dp
+ 160dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 70b3051f3..e0b797092 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -471,8 +471,7 @@
ends at %sclosed
-
- <b>%1$d%%</b> %2$s
+ <b>%1$d%%</b> %2$sVote
@@ -497,4 +496,15 @@
%d seconds
+ Continue
+ Back
+ Done
+ Successfully reported @%s
+ Additional comments
+ Forward to %s
+ Failed to report
+ Failed to fetch statuses
+ The report will be sent to your server moderator. You can provide an explanation of why you are reporting this account below:
+ The account is from another server. Send an anonymized copy of the report there as well?
+