mirror of https://github.com/tuskyapp/Tusky.git
Browse Source
* 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 adapterpull/1315/head^2
39 changed files with 2726 additions and 416 deletions
@ -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); |
||||
} |
||||
} |
||||
@ -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; |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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() |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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) |
||||
} |
||||
@ -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 |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
|
||||
) |
||||
} |
||||
} |
||||
@ -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() |
||||
} |
||||
|
||||
} |
||||
@ -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() |
||||
} |
||||
|
||||
} |
||||
@ -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() |
||||
} |
||||
} |
||||
@ -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) |
||||
} |
||||
@ -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) |
||||
@ -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) |
||||
} |
||||
|
||||
@ -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) |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
Loading…
Reference in new issue