Redesign report activity (#1295)

* Report activity core

* Implement navigation

* Implement navigation

* Update strings

* Revert manifest formatting

* Implement Done page

* Add landscape layout

* Implement Note fragment

* Create component

* Implement simple status adapter

* Format code

* Add date/time to report statuses

* Refactor status view holder

* Refactor code

* Refactor ViewPager

* Replace MaterialButton with Button

* Remove unneeded string

* Update Text and Check views style

* Remove old ReportActivity and rename Report2Activity to ReportActivity

* Hide "report to remote instance" checkbox for local accounts

* Add account, hashtag and links click handler

* Add media preview

* Add sensitive content support

* Add status expand/collapse support

* Update adapter to user adapterPosition instead of stored status

* Updated checked change handling

* Add polls support to report screen

* Add copyright

* Set buttonTint at CheckBox

* Exclude reblogs from statuses for reports

* Change final page check mark size

* Update report note screen

* Fix typos

* Remove unused params from api endpoint

* Replace .visibility with show()/hide()

* Replace Date().time with System.currentTime...

* Add line spacing

* Fix close button tint issue

* Updated status adapter
This commit is contained in:
pandasoft0 2019-06-09 17:55:34 +03:00 committed by Konrad Pozniak
parent f7581daa75
commit c335651b6b
39 changed files with 2726 additions and 416 deletions

View File

@ -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'
}

View File

@ -101,9 +101,7 @@
<activity android:name=".AccountListActivity" />
<activity android:name=".AboutActivity" />
<activity android:name=".TabPreferenceActivity" />
<activity
android:name=".ReportActivity"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
android:theme="@style/Base.Theme.AppCompat" />
@ -119,6 +117,8 @@
<activity android:name=".ModalTimelineActivity" />
<activity android:name=".LicenseActivity" />
<activity android:name=".FiltersActivity" />
<activity android:name=".components.report.ReportActivity"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />

View File

@ -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 <http://www.gnu.org/licenses>. */
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<ResponseBody> callback = new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response.isSuccessful()) {
onSendSuccess();
} else {
onSendFailure(accountId, statusIds, comment);
}
}
@Override
public void onFailure(Call<ResponseBody> 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<List<Status>> callback = new Callback<List<Status>>() {
@Override
public void onResponse(Call<List<Status>> call, Response<List<Status>> response) {
if (!response.isSuccessful()) {
onFetchStatusesFailure(new Exception(response.message()));
return;
}
List<Status> statusList = response.body();
List<ReportAdapter.ReportStatus> 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<List<Status>> 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);
}
}

View File

@ -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<Fragment> 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);

View File

@ -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 <http://www.gnu.org/licenses>. */
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<ReportStatus> 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<ReportStatus> 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<String> 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;
}
});
}
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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<Fragment>
@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<Fragment> = dispatchingFragmentInjector
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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<Screen>()
val navigation: LiveData<Screen> = navigationMutable
private val muteStateMutable = MutableLiveData<Resource<Boolean>>()
val muteState: LiveData<Resource<Boolean>> = muteStateMutable
private val blockStateMutable = MutableLiveData<Resource<Boolean>>()
val blockState: LiveData<Resource<Boolean>> = blockStateMutable
private val reportingStateMutable = MutableLiveData<Resource<Boolean>>()
var reportingState: LiveData<Resource<Boolean>> = reportingStateMutable
private val checkUrlMutable = MutableLiveData<String>()
val checkUrl: LiveData<String> = checkUrlMutable
private val repoResult = MutableLiveData<BiListing<Status>>()
val statuses: LiveData<PagedList<Status>> = Transformations.switchMap(repoResult) { it.pagedList }
val networkStateAfter: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkStateAfter }
val networkStateBefore: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkStateBefore }
val networkStateRefresh: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.refreshState }
private val selectedIds = HashSet<String>()
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<Relationship> = 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<Relationship> = 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()
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.report
enum class Screen {
Statuses,
Note,
Done,
Back,
Finish
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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<Status.Mention>?,
emojis: List<Emoji>,
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)
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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, RecyclerView.ViewHolder>(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<Status>() {
override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
oldItem == newItem
override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean =
oldItem.id == newItem.id
}
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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<String, Status>() {
val networkStateAfter = MutableLiveData<NetworkState>()
val networkStateBefore = MutableLiveData<NetworkState>()
private var retryAfter: (() -> Any)? = null
private var retryBefore: (() -> Any)? = null
private var retryInitial: (() -> Any)? = null
val initialLoad = MutableLiveData<NetworkState>()
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<String>, callback: LoadInitialCallback<Status>) {
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<Status> ->
val ret = ArrayList<Status>()
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<String>, callback: LoadCallback<Status>) {
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<String>, callback: LoadCallback<Status>) {
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
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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<String, Status>() {
val sourceLiveData = MutableLiveData<StatusesDataSource>()
override fun create(): DataSource<String, Status> {
val source = StatusesDataSource(accountId, mastodonApi, disposables, retryExecutor)
sourceLiveData.postValue(source)
return source
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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<Status> {
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
}
)
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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()
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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()
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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<PagedList<Status>> {
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()
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.report.model
class StatusViewState {
private val mediaShownState = HashMap<String, Boolean>()
private val contentShownState = HashMap<String, Boolean>()
private val longContentCollapsedState = HashMap<String, Boolean>()
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<String, Boolean>, id: String, def: Boolean): Boolean = map[id]
?: def
private fun setStateEnabled(map: MutableMap<String, Boolean>, id: String, state: Boolean) = map.put(id, state)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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) {

View File

@ -385,4 +385,40 @@ public interface MastodonApi {
@Path("id") String id,
@Field("choices[]") List<Integer> choices
);
@POST("api/v1/accounts/{id}/block")
Single<Relationship> blockAccountObservable(@Path("id") String accountId);
@POST("api/v1/accounts/{id}/unblock")
Single<Relationship> unblockAccountObservable(@Path("id") String accountId);
@POST("api/v1/accounts/{id}/mute")
Single<Relationship> muteAccountObservable(@Path("id") String accountId);
@POST("api/v1/accounts/{id}/unmute")
Single<Relationship> unmuteAccountObservable(@Path("id") String accountId);
@GET("api/v1/accounts/relationships")
Single<List<Relationship>> relationshipsObservable(@Query("id[]") List<String> accountIds);
@FormUrlEncoded
@POST("api/v1/reports")
Single<ResponseBody> reportObservable(
@Field("account_id") String accountId,
@Field("status_ids[]") List<String> statusIds,
@Field("comment") String comment,
@Field("forward") Boolean isNotifyRemote);
@GET("api/v1/accounts/{id}/statuses")
Single<List<Status>> 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<Status> statusObservable(@Path("id") String statusId);
}

View File

@ -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<T>(
// the LiveData of paged lists for the UI to observe
val pagedList: LiveData<PagedList<T>>,
// represents the network request status for load data before first to show to the user
val networkStateBefore: LiveData<NetworkState>,
// represents the network request status for load data after last to show to the user
val networkStateAfter: LiveData<NetworkState>,
// represents the refresh status to show to the user. Separate from networkState, this
// value is importantly only when refresh is requested.
val refreshState: LiveData<NetworkState>,
// refreshes the whole data and fetches it from scratch.
val refresh: () -> Unit,
// retries any failed requests.
val retry: () -> Unit)

View File

@ -8,5 +8,6 @@ class Success<T> (override val data: T? = null) : Resource<T>(data)
class Error<T> (override val data: T? = null,
val errorMessage: String? = null,
var consumed: Boolean = false
var consumed: Boolean = false,
val cause: Throwable? = null
): Resource<T>(data)

View File

@ -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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util
import com.keylesspalace.tusky.entity.Status
fun Status.isCollapsible(): Boolean {
return !SmartLengthInputFilter.hasBadRatio(content, SmartLengthInputFilter.LENGTH_DEFAULT)
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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<Attachment>,
sensitive: Boolean,
previewListener: MediaPreviewListener,
showingContent: Boolean,
mediaPreviewHeight: Int) {
val context = itemView.context
val mediaPreviews = arrayOf<MediaPreviewImageView>(
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<ImageView>(
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<TextView>(R.id.status_sensitive_media_warning)
val sensitiveMediaShow = itemView.findViewById<View>(R.id.status_sensitive_media_button)
val mediaLabel = itemView.findViewById<TextView>(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<Attachment>, 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<Emoji>, useAbsoluteTime: Boolean) {
val pollResults = listOf<TextView>(
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<TextView>(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<Emoji>, pollResults: List<TextView>) {
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<InputFilter>(SmartLengthInputFilter.INSTANCE)
val NO_INPUT_FILTER = arrayOfNulls<InputFilter>(0)
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/tusky_blue" />
</shape>

View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".components.report.fragments.ReportStatusesFragment">
<View
android:id="@+id/checkMark"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="48dp"
android:background="@drawable/report_success_background"
app:layout_constraintDimensionRatio="W,1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHeight_default="percent"
app:layout_constraintHeight_percent="0.4" />
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_check_24dp"
app:layout_constraintBottom_toBottomOf="@id/checkMark"
app:layout_constraintEnd_toEndOf="@id/checkMark"
app:layout_constraintHeight_percent="0.25"
app:layout_constraintStart_toStartOf="@id/checkMark"
app:layout_constraintTop_toTopOf="@id/checkMark"
app:layout_constraintHeight_default="percent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/textReported"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:gravity="center_horizontal"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_large"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/checkMark"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="48dp"
android:layout_marginEnd="16dp"/>
<Button
android:id="@+id/buttonMute"
style="@style/TuskyButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="@dimen/min_report_button_width"
android:text="@string/action_mute"
app:layout_constraintBottom_toTopOf="@id/buttonBlock"
app:layout_constraintEnd_toEndOf="@id/textReported"
app:layout_constraintStart_toStartOf="@id/textReported"
app:layout_constraintTop_toBottomOf="@id/textReported"
app:layout_constraintVertical_chainStyle="packed" />
<ProgressBar
android:id="@+id/progressMute"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@id/buttonMute"
app:layout_constraintEnd_toEndOf="@id/buttonMute"
app:layout_constraintStart_toStartOf="@id/buttonMute"
app:layout_constraintTop_toTopOf="@id/buttonMute" />
<Button
android:id="@+id/buttonBlock"
style="@style/TuskyButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="@dimen/min_report_button_width"
android:text="@string/action_block"
app:layout_constraintBottom_toTopOf="@id/buttonDone"
app:layout_constraintEnd_toEndOf="@id/textReported"
app:layout_constraintStart_toStartOf="@id/textReported"
app:layout_constraintTop_toBottomOf="@id/buttonMute"
app:layout_constraintVertical_chainStyle="packed" />
<ProgressBar
android:id="@+id/progressBlock"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@id/buttonBlock"
app:layout_constraintEnd_toEndOf="@id/buttonBlock"
app:layout_constraintStart_toStartOf="@id/buttonBlock"
app:layout_constraintTop_toTopOf="@id/buttonBlock" />
<Button
android:id="@+id/buttonDone"
style="@style/TuskyButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:minWidth="@dimen/min_report_button_width"
android:text="@string/button_done"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/textReported"
app:layout_constraintStart_toStartOf="@id/textReported"
app:layout_constraintTop_toBottomOf="@id/buttonBlock"
app:layout_constraintVertical_chainStyle="packed" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,40 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_view_thread"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
tools:context=".components.report.ReportActivity">
<include layout="@layout/toolbar_basic" />
<LinearLayout
<com.keylesspalace.tusky.view.NoSwipeViewPager
android:id="@+id/wizard"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
android:overScrollMode="never"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:id="@+id/report_recycler_view"
android:scrollbars="vertical"
android:fadeScrollbars="false"
android:background="?attr/report_status_background_color" />
<androidx.emoji.widget.EmojiEditText
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/report_comment"
android:inputType="textMultiLine"
android:gravity="top|start"
android:background="@android:color/transparent"
android:ems="10"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:hint="@string/report_comment_hint" />
</LinearLayout>
<include layout="@layout/item_status_bottom_sheet" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".components.report.fragments.ReportStatusesFragment">
<View
android:id="@+id/checkMark"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="56dp"
android:background="@drawable/report_success_background"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent="0.35" />
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_check_24dp"
app:layout_constraintBottom_toBottomOf="@id/checkMark"
app:layout_constraintEnd_toEndOf="@id/checkMark"
app:layout_constraintHeight_percent="0.3"
app:layout_constraintStart_toStartOf="@id/checkMark"
app:layout_constraintTop_toTopOf="@id/checkMark"
app:layout_constraintWidth_percent="0.22"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/textReported"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:gravity="center_horizontal"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_large"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/checkMark"
app:layout_constraintWidth_percent="0.9" />
<Button
android:id="@+id/buttonMute"
style="@style/TuskyButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="@dimen/min_report_button_width"
android:text="@string/action_mute"
app:layout_constraintBottom_toTopOf="@id/buttonBlock"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textReported"
app:layout_constraintVertical_chainStyle="packed" />
<ProgressBar
android:id="@+id/progressMute"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@id/buttonMute"
app:layout_constraintEnd_toEndOf="@id/buttonMute"
app:layout_constraintStart_toStartOf="@id/buttonMute"
app:layout_constraintTop_toTopOf="@id/buttonMute" />
<Button
android:id="@+id/buttonBlock"
style="@style/TuskyButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="@dimen/min_report_button_width"
android:text="@string/action_block"
app:layout_constraintBottom_toTopOf="@id/buttonDone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/buttonMute"
app:layout_constraintVertical_chainStyle="packed" />
<ProgressBar
android:id="@+id/progressBlock"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@id/buttonBlock"
app:layout_constraintEnd_toEndOf="@id/buttonBlock"
app:layout_constraintStart_toStartOf="@id/buttonBlock"
app:layout_constraintTop_toTopOf="@id/buttonBlock" />
<Button
android:id="@+id/buttonDone"
style="@style/TuskyButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:minWidth="@dimen/min_report_button_width"
android:text="@string/button_done"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/buttonBlock"
app:layout_constraintVertical_chainStyle="packed" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".components.report.fragments.ReportStatusesFragment">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/buttonReport"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideBegin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideEnd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<TextView
android:id="@+id/reportDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/report_description_1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_small"
app:layout_constraintEnd_toEndOf="@id/guideEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin"
android:lineSpacingMultiplier="1.1"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layoutAdditionalInfo"
style="@style/TuskyTextInput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/hint_additional_info"
app:hintEnabled="true"
app:layout_constraintEnd_toEndOf="@id/guideEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/reportDescription">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editNote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:inputType="textCapSentences|textMultiLine"
android:minLines="4" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/reportDescriptionRemoteInstance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/report_description_remote_instance"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_small"
android:lineSpacingMultiplier="1.1"
app:layout_constraintEnd_toEndOf="@id/guideEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/layoutAdditionalInfo" />
<CheckBox
android:id="@+id/checkIsNotifyRemote"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/report_remote_instance"
android:textSize="?attr/status_text_medium"
app:buttonTint="?attr/compound_button_color"
app:layout_constraintEnd_toEndOf="@id/guideEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/reportDescriptionRemoteInstance" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/checkIsNotifyRemote" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<Button
android:id="@+id/buttonBack"
style="@style/TuskyButton.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/button_back"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/buttonReport" />
<Button
android:id="@+id/buttonReport"
style="@style/TuskyButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/action_report"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".components.report.fragments.ReportStatusesFragment">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/buttonContinue"
app:layout_constraintTop_toTopOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<ProgressBar
android:id="@+id/progressBarTop"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintTop_toTopOf="@id/swipeRefreshLayout" />
<ProgressBar
android:id="@+id/progressBarBottom"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="@id/swipeRefreshLayout" />
<ProgressBar
android:id="@+id/progressBarLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/swipeRefreshLayout"
app:layout_constraintEnd_toEndOf="@id/swipeRefreshLayout"
app:layout_constraintStart_toStartOf="@id/swipeRefreshLayout"
app:layout_constraintTop_toTopOf="@id/swipeRefreshLayout" />
<Button
android:id="@+id/buttonCancel"
style="@style/TuskyButton.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@android:string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/buttonContinue" />
<Button
android:id="@+id/buttonContinue"
style="@style/TuskyButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/button_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,22 +1,357 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/report_status_content"
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideBegin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="8dp" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/statusContentWarningDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/barrierEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toTopOf="parent"
tools:text="content warning which is very long and it doesn't fit"
tools:visibility="visible" />
<ToggleButton
android:id="@+id/statusContentWarningButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:background="?attr/content_warning_button"
android:minWidth="150dp"
android:minHeight="0dp"
android:paddingLeft="16dp"
android:paddingTop="4dp"
android:paddingRight="16dp"
android:paddingBottom="4dp"
android:textAllCaps="true"
android:textOff="@string/status_content_warning_show_more"
android:textOn="@string/status_content_warning_show_less"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/statusContentWarningDescription"
tools:visibility="visible" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/statusContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:padding="8dp"
android:textSize="?attr/status_text_medium" />
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" />
<ToggleButton
android:id="@+id/buttonToggleContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:background="?attr/content_warning_button"
android:minWidth="150dp"
android:minHeight="0dp"
android:paddingLeft="16dp"
android:paddingTop="4dp"
android:paddingRight="16dp"
android:paddingBottom="4dp"
android:textAllCaps="true"
android:textOff="@string/status_content_show_less"
android:textOn="@string/status_content_show_more"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/statusContent"
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/status_media_preview_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/status_media_preview_margin_top"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@id/barrierEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/buttonToggleContent"
tools:visibility="gone">
<com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_0"
android:layout_width="0dp"
android:layout_height="@dimen/status_media_preview_height"
android:scaleType="centerCrop"
app:layout_constraintEnd_toStartOf="@+id/status_media_preview_1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_1"
android:layout_width="0dp"
android:layout_height="@dimen/status_media_preview_height"
android:layout_marginStart="4dp"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/status_media_preview_0"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_2"
android:layout_width="0dp"
android:layout_height="@dimen/status_media_preview_height"
android:layout_marginTop="4dp"
android:scaleType="centerCrop"
app:layout_constraintEnd_toStartOf="@+id/status_media_preview_3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/status_media_preview_0"
tools:ignore="ContentDescription" />
<com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_3"
android:layout_width="0dp"
android:layout_height="@dimen/status_media_preview_height"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/status_media_preview_2"
app:layout_constraintTop_toBottomOf="@+id/status_media_preview_1"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/status_media_overlay_0"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_0"
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_0"
app:layout_constraintStart_toStartOf="@+id/status_media_preview_0"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_0"
app:srcCompat="?attr/play_indicator_drawable"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/status_media_overlay_1"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_1"
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_1"
app:layout_constraintStart_toStartOf="@+id/status_media_preview_1"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_1"
app:srcCompat="?attr/play_indicator_drawable"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/status_media_overlay_2"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_2"
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_2"
app:layout_constraintStart_toStartOf="@+id/status_media_preview_2"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_2"
app:srcCompat="?attr/play_indicator_drawable"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/status_media_overlay_3"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_3"
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_3"
app:layout_constraintStart_toStartOf="@+id/status_media_preview_3"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_3"
app:srcCompat="?attr/play_indicator_drawable"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/status_sensitive_media_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.7"
android:contentDescription="@null"
android:padding="@dimen/status_sensitive_media_button_padding"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_container"
app:srcCompat="@drawable/ic_eye_24dp" />
<TextView
android:id="@+id/status_sensitive_media_warning"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?attr/sensitive_media_warning_background_color"
android:gravity="center"
android:lineSpacingMultiplier="1.2"
android:orientation="vertical"
android:padding="8dp"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/status_media_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawablePadding="4dp"
android:gravity="center_vertical"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.emoji.widget.EmojiTextView
android:id="@+id/status_poll_option_result_0"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginEnd="8dp"
android:background="@drawable/poll_option_background"
android:ellipsize="end"
android:lines="1"
android:paddingStart="6dp"
android:paddingTop="2dp"
android:paddingEnd="6dp"
android:paddingBottom="2dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toStartOf="@id/barrierEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/status_media_preview_container"
tools:text="40%" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/status_poll_option_result_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginEnd="8dp"
android:background="@drawable/poll_option_background"
android:ellipsize="end"
android:lines="1"
android:paddingStart="6dp"
android:paddingTop="2dp"
android:paddingEnd="6dp"
android:paddingBottom="2dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toStartOf="@id/barrierEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_0"
tools:text="10%" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/status_poll_option_result_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginEnd="8dp"
android:background="@drawable/poll_option_background"
android:ellipsize="end"
android:lines="1"
android:paddingStart="6dp"
android:paddingTop="2dp"
android:paddingEnd="6dp"
android:paddingBottom="2dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toStartOf="@id/barrierEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_1"
tools:text="20%" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/status_poll_option_result_3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginEnd="8dp"
android:background="@drawable/poll_option_background"
android:ellipsize="end"
android:lines="1"
android:paddingStart="6dp"
android:paddingTop="2dp"
android:paddingEnd="6dp"
android:paddingBottom="2dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toStartOf="@id/barrierEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_2"
tools:text="30%" />
<TextView
android:id="@+id/status_poll_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="@id/barrierEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_3"
tools:text="7 votes • 7 hours remaining" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrierEnd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start"
app:constraint_referenced_ids="statusSelection,timestampInfo" />
<TextView
android:id="@+id/timestampInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:importantForAccessibility="no"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/barrierEnd"
app:layout_constraintTop_toTopOf="parent"
tools:text="21 Dec 2018 18:45" />
<CheckBox
android:id="@+id/report_status_check_box"
android:id="@+id/statusSelection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="16dp" />
android:layout_margin="16dp"
app:buttonTint="?attr/compound_button_color"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/timestampInfo" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -46,5 +46,4 @@
<attr name="status_text_large" format="dimension" />
<attr name="pollOptionBackgroundColor" format="reference|color" />
</resources>

View File

@ -39,4 +39,5 @@
<dimen name="avatar_radius_42dp">5.25dp</dimen> <!-- 1/8 of 42dp -->
<dimen name="avatar_radius_36dp">4.5dp</dimen> <!-- 1/8 of 36dp -->
<dimen name="avatar_radius_24dp">3dp</dimen> <!-- 1/8 of 24dp -->
<dimen name="min_report_button_width">160dp</dimen>
</resources>

View File

@ -471,8 +471,7 @@
<string name="poll_info_time_absolute">ends at %s</string>
<string name="poll_info_closed">closed</string>
<string name="poll_option_format">
<!-- 15% vote for this! -->
&lt;b>%1$d%%&lt;/b> %2$s</string>
<!-- 15% vote for this! --> &lt;b>%1$d%%&lt;/b> %2$s</string>
<string name="poll_vote">Vote</string>
@ -497,4 +496,15 @@
<item quantity="other">%d seconds</item>
</plurals>
<string name="button_continue">Continue</string>
<string name="button_back">Back</string>
<string name="button_done">Done</string>
<string name="report_sent_success">Successfully reported @%s</string>
<string name="hint_additional_info">Additional comments</string>
<string name="report_remote_instance">Forward to %s</string>
<string name="failed_report">Failed to report</string>
<string name="failed_fetch_statuses">Failed to fetch statuses</string>
<string name="report_description_1">The report will be sent to your server moderator. You can provide an explanation of why you are reporting this account below:</string>
<string name="report_description_remote_instance">The account is from another server. Send an anonymized copy of the report there as well?</string>
</resources>