Redesign report activity (#1295)
* Report activity core * Implement navigation * Implement navigation * Update strings * Revert manifest formatting * Implement Done page * Add landscape layout * Implement Note fragment * Create component * Implement simple status adapter * Format code * Add date/time to report statuses * Refactor status view holder * Refactor code * Refactor ViewPager * Replace MaterialButton with Button * Remove unneeded string * Update Text and Check views style * Remove old ReportActivity and rename Report2Activity to ReportActivity * Hide "report to remote instance" checkbox for local accounts * Add account, hashtag and links click handler * Add media preview * Add sensitive content support * Add status expand/collapse support * Update adapter to user adapterPosition instead of stored status * Updated checked change handling * Add polls support to report screen * Add copyright * Set buttonTint at CheckBox * Exclude reblogs from statuses for reports * Change final page check mark size * Update report note screen * Fix typos * Remove unused params from api endpoint * Replace .visibility with show()/hide() * Replace Date().time with System.currentTime... * Add line spacing * Fix close button tint issue * Updated status adapter
This commit is contained in:
parent
f7581daa75
commit
c335651b6b
@ -6,7 +6,7 @@ apply plugin: 'kotlin-kapt'
|
||||
def getGitSha = { ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'rev-parse', '--short' , 'HEAD'
|
||||
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
@ -35,15 +35,15 @@ android {
|
||||
shrinkResources true
|
||||
proguardFiles 'proguard-rules.pro'
|
||||
}
|
||||
debug { }
|
||||
debug {}
|
||||
}
|
||||
|
||||
flavorDimensions "color"
|
||||
productFlavors {
|
||||
blue { }
|
||||
blue {}
|
||||
green {
|
||||
applicationIdSuffix ".test"
|
||||
versionNameSuffix "-"+getGitSha()
|
||||
versionNameSuffix "-" + getGitSha()
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +124,7 @@ dependencies {
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||
//room
|
||||
implementation 'androidx.room:room-runtime:2.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
kapt 'androidx.room:room-compiler:2.0.0'
|
||||
implementation 'androidx.room:room-rxjava2:2.0.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
@ -153,4 +154,7 @@ dependencies {
|
||||
//Glide
|
||||
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.9.0'
|
||||
|
||||
//Add some useful extensions
|
||||
implementation 'androidx.core:core-ktx:1.2.0-alpha01'
|
||||
}
|
||||
|
@ -101,9 +101,7 @@
|
||||
<activity android:name=".AccountListActivity" />
|
||||
<activity android:name=".AboutActivity" />
|
||||
<activity android:name=".TabPreferenceActivity" />
|
||||
<activity
|
||||
android:name=".ReportActivity"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
||||
android:theme="@style/Base.Theme.AppCompat" />
|
||||
@ -119,6 +117,8 @@
|
||||
<activity android:name=".ModalTimelineActivity" />
|
||||
<activity android:name=".LicenseActivity" />
|
||||
<activity android:name=".FiltersActivity" />
|
||||
<activity android:name=".components.report.ReportActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
|
||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -15,6 +15,8 @@
|
||||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@ -34,16 +36,23 @@ import dagger.android.DispatchingAndroidInjector;
|
||||
import dagger.android.support.HasSupportFragmentInjector;
|
||||
|
||||
public class ViewTagActivity extends BottomSheetActivity implements HasSupportFragmentInjector {
|
||||
private static final String HASHTAG = "hashtag";
|
||||
|
||||
@Inject
|
||||
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
|
||||
|
||||
public static Intent getIntent(Context context, String tag){
|
||||
Intent intent = new Intent(context,ViewTagActivity.class);
|
||||
intent.putExtra(HASHTAG,tag);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_view_tag);
|
||||
|
||||
String hashtag = getIntent().getStringExtra("hashtag");
|
||||
String hashtag = getIntent().getStringExtra(HASHTAG);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
@ -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)
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
package com.keylesspalace.tusky.di
|
||||
|
||||
import com.keylesspalace.tusky.*
|
||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
@ -71,9 +72,6 @@ abstract class ActivitiesModule {
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesSplashActivity(): SplashActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesReportActivity(): ReportActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesSavedTootActivity(): SavedTootActivity
|
||||
|
||||
@ -92,4 +90,6 @@ abstract class ActivitiesModule {
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesFiltersActivity(): FiltersActivity
|
||||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesReportActivity(): ReportActivity
|
||||
}
|
||||
|
@ -21,6 +21,9 @@ import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||
import com.keylesspalace.tusky.fragment.*
|
||||
import com.keylesspalace.tusky.fragment.preference.AccountPreferencesFragment
|
||||
import com.keylesspalace.tusky.fragment.preference.NotificationPreferencesFragment
|
||||
import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment
|
||||
import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment
|
||||
import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
@ -60,4 +63,12 @@ abstract class FragmentBuildersModule {
|
||||
@ContributesAndroidInjector
|
||||
abstract fun accountInListsFragment(): AccountsInListFragment
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun reportStatusesFragment(): ReportStatusesFragment
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun reportNoteFragment(): ReportNoteFragment
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun reportDoneFragment(): ReportDoneFragment
|
||||
}
|
@ -4,10 +4,9 @@ package com.keylesspalace.tusky.di
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.*
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
||||
import dagger.Binds
|
||||
import dagger.MapKey
|
||||
@ -61,5 +60,10 @@ abstract class ViewModelModule {
|
||||
@ViewModelKey(AccountsInListViewModel::class)
|
||||
internal abstract fun accountsInListViewModel(viewModel: AccountsInListViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ReportViewModel::class)
|
||||
internal abstract fun reportViewModel(viewModel: ReportViewModel): ViewModel
|
||||
|
||||
//Add more ViewModels here
|
||||
}
|
@ -44,9 +44,9 @@ import com.keylesspalace.tusky.BottomSheetActivity;
|
||||
import com.keylesspalace.tusky.ComposeActivity;
|
||||
import com.keylesspalace.tusky.MainActivity;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.ReportActivity;
|
||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||
import com.keylesspalace.tusky.ViewTagActivity;
|
||||
import com.keylesspalace.tusky.components.report.ReportActivity;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
import com.keylesspalace.tusky.di.Injectable;
|
||||
@ -54,7 +54,6 @@ import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.network.TimelineCases;
|
||||
import com.keylesspalace.tusky.util.HtmlUtils;
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
@ -327,12 +326,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
||||
|
||||
protected void openReportPage(String accountId, String accountUsername, String statusId,
|
||||
Spanned statusContent) {
|
||||
Intent intent = new Intent(getContext(), ReportActivity.class);
|
||||
intent.putExtra("account_id", accountId);
|
||||
intent.putExtra("account_username", accountUsername);
|
||||
intent.putExtra("status_id", statusId);
|
||||
intent.putExtra("status_content", HtmlUtils.toHtml(statusContent));
|
||||
startActivity(intent);
|
||||
startActivity(ReportActivity.getIntent(requireContext(),accountId,accountUsername,statusId,statusContent));
|
||||
}
|
||||
|
||||
protected void showConfirmDeleteDialog(final String id, final int position) {
|
||||
|
@ -385,4 +385,40 @@ public interface MastodonApi {
|
||||
@Path("id") String id,
|
||||
@Field("choices[]") List<Integer> choices
|
||||
);
|
||||
|
||||
@POST("api/v1/accounts/{id}/block")
|
||||
Single<Relationship> blockAccountObservable(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/unblock")
|
||||
Single<Relationship> unblockAccountObservable(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/mute")
|
||||
Single<Relationship> muteAccountObservable(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/unmute")
|
||||
Single<Relationship> unmuteAccountObservable(@Path("id") String accountId);
|
||||
|
||||
@GET("api/v1/accounts/relationships")
|
||||
Single<List<Relationship>> relationshipsObservable(@Query("id[]") List<String> accountIds);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/reports")
|
||||
Single<ResponseBody> reportObservable(
|
||||
@Field("account_id") String accountId,
|
||||
@Field("status_ids[]") List<String> statusIds,
|
||||
@Field("comment") String comment,
|
||||
@Field("forward") Boolean isNotifyRemote);
|
||||
|
||||
@GET("api/v1/accounts/{id}/statuses")
|
||||
Single<List<Status>> accountStatusesObservable(
|
||||
@Path("id") String accountId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit,
|
||||
@Nullable @Query("exclude_reblogs") Boolean excludeReblogs);
|
||||
|
||||
|
||||
@GET("api/v1/statuses/{id}")
|
||||
Single<Status> statusObservable(@Path("id") String statusId);
|
||||
|
||||
}
|
||||
|
38
app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt
Normal file
38
app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.util
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
|
||||
/**
|
||||
* Data class that is necessary for a UI to show a listing and interact w/ the rest of the system
|
||||
*/
|
||||
data class BiListing<T>(
|
||||
// the LiveData of paged lists for the UI to observe
|
||||
val pagedList: LiveData<PagedList<T>>,
|
||||
// represents the network request status for load data before first to show to the user
|
||||
val networkStateBefore: LiveData<NetworkState>,
|
||||
// represents the network request status for load data after last to show to the user
|
||||
val networkStateAfter: LiveData<NetworkState>,
|
||||
// represents the refresh status to show to the user. Separate from networkState, this
|
||||
// value is importantly only when refresh is requested.
|
||||
val refreshState: LiveData<NetworkState>,
|
||||
// refreshes the whole data and fetches it from scratch.
|
||||
val refresh: () -> Unit,
|
||||
// retries any failed requests.
|
||||
val retry: () -> Unit)
|
@ -8,5 +8,6 @@ class Success<T> (override val data: T? = null) : Resource<T>(data)
|
||||
|
||||
class Error<T> (override val data: T? = null,
|
||||
val errorMessage: String? = null,
|
||||
var consumed: Boolean = false
|
||||
var consumed: Boolean = false,
|
||||
val cause: Throwable? = null
|
||||
): Resource<T>(data)
|
@ -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
|
||||
}
|
||||
}
|
4
app/src/main/res/drawable/report_success_background.xml
Normal file
4
app/src/main/res/drawable/report_success_background.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
|
||||
<solid android:color="@color/tusky_blue" />
|
||||
</shape>
|
109
app/src/main/res/layout-land/fragment_report_done.xml
Normal file
109
app/src/main/res/layout-land/fragment_report_done.xml
Normal file
@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".components.report.fragments.ReportStatusesFragment">
|
||||
|
||||
<View
|
||||
android:id="@+id/checkMark"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="48dp"
|
||||
android:background="@drawable/report_success_background"
|
||||
app:layout_constraintDimensionRatio="W,1:1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintHeight_default="percent"
|
||||
app:layout_constraintHeight_percent="0.4" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_check_24dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/checkMark"
|
||||
app:layout_constraintEnd_toEndOf="@id/checkMark"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintStart_toStartOf="@id/checkMark"
|
||||
app:layout_constraintTop_toTopOf="@id/checkMark"
|
||||
app:layout_constraintHeight_default="percent"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textReported"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="?attr/status_text_large"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/checkMark"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginStart="48dp"
|
||||
android:layout_marginEnd="16dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonMute"
|
||||
style="@style/TuskyButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/min_report_button_width"
|
||||
android:text="@string/action_mute"
|
||||
app:layout_constraintBottom_toTopOf="@id/buttonBlock"
|
||||
app:layout_constraintEnd_toEndOf="@id/textReported"
|
||||
app:layout_constraintStart_toStartOf="@id/textReported"
|
||||
app:layout_constraintTop_toBottomOf="@id/textReported"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressMute"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@id/buttonMute"
|
||||
app:layout_constraintEnd_toEndOf="@id/buttonMute"
|
||||
app:layout_constraintStart_toStartOf="@id/buttonMute"
|
||||
app:layout_constraintTop_toTopOf="@id/buttonMute" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonBlock"
|
||||
style="@style/TuskyButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/min_report_button_width"
|
||||
android:text="@string/action_block"
|
||||
app:layout_constraintBottom_toTopOf="@id/buttonDone"
|
||||
app:layout_constraintEnd_toEndOf="@id/textReported"
|
||||
app:layout_constraintStart_toStartOf="@id/textReported"
|
||||
app:layout_constraintTop_toBottomOf="@id/buttonMute"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBlock"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@id/buttonBlock"
|
||||
app:layout_constraintEnd_toEndOf="@id/buttonBlock"
|
||||
app:layout_constraintStart_toStartOf="@id/buttonBlock"
|
||||
app:layout_constraintTop_toTopOf="@id/buttonBlock" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonDone"
|
||||
style="@style/TuskyButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:minWidth="@dimen/min_report_button_width"
|
||||
android:text="@string/button_done"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/textReported"
|
||||
app:layout_constraintStart_toStartOf="@id/textReported"
|
||||
app:layout_constraintTop_toBottomOf="@id/buttonBlock"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -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>
|
108
app/src/main/res/layout/fragment_report_done.xml
Normal file
108
app/src/main/res/layout/fragment_report_done.xml
Normal file
@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".components.report.fragments.ReportStatusesFragment">
|
||||
|
||||
<View
|
||||
android:id="@+id/checkMark"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="56dp"
|
||||
android:background="@drawable/report_success_background"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_default="percent"
|
||||
app:layout_constraintWidth_percent="0.35" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_check_24dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/checkMark"
|
||||
app:layout_constraintEnd_toEndOf="@id/checkMark"
|
||||
app:layout_constraintHeight_percent="0.3"
|
||||
app:layout_constraintStart_toStartOf="@id/checkMark"
|
||||
app:layout_constraintTop_toTopOf="@id/checkMark"
|
||||
app:layout_constraintWidth_percent="0.22"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textReported"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="?attr/status_text_large"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/checkMark"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonMute"
|
||||
style="@style/TuskyButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/min_report_button_width"
|
||||
android:text="@string/action_mute"
|
||||
app:layout_constraintBottom_toTopOf="@id/buttonBlock"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textReported"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressMute"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@id/buttonMute"
|
||||
app:layout_constraintEnd_toEndOf="@id/buttonMute"
|
||||
app:layout_constraintStart_toStartOf="@id/buttonMute"
|
||||
app:layout_constraintTop_toTopOf="@id/buttonMute" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonBlock"
|
||||
style="@style/TuskyButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/min_report_button_width"
|
||||
android:text="@string/action_block"
|
||||
app:layout_constraintBottom_toTopOf="@id/buttonDone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/buttonMute"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBlock"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@id/buttonBlock"
|
||||
app:layout_constraintEnd_toEndOf="@id/buttonBlock"
|
||||
app:layout_constraintStart_toStartOf="@id/buttonBlock"
|
||||
app:layout_constraintTop_toTopOf="@id/buttonBlock" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonDone"
|
||||
style="@style/TuskyButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:minWidth="@dimen/min_report_button_width"
|
||||
android:text="@string/button_done"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/buttonBlock"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
129
app/src/main/res/layout/fragment_report_note.xml
Normal file
129
app/src/main/res/layout/fragment_report_note.xml
Normal file
@ -0,0 +1,129 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".components.report.fragments.ReportStatusesFragment">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/buttonReport"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideBegin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="16dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideEnd"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_end="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reportDescription"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/report_description_1"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="?attr/status_text_small"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/guideBegin"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layoutAdditionalInfo"
|
||||
style="@style/TuskyTextInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/hint_additional_info"
|
||||
app:hintEnabled="true"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/guideBegin"
|
||||
app:layout_constraintTop_toBottomOf="@id/reportDescription">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/editNote"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:inputType="textCapSentences|textMultiLine"
|
||||
android:minLines="4" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reportDescriptionRemoteInstance"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/report_description_remote_instance"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="?attr/status_text_small"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/guideBegin"
|
||||
app:layout_constraintTop_toBottomOf="@id/layoutAdditionalInfo" />
|
||||
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkIsNotifyRemote"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/report_remote_instance"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:buttonTint="?attr/compound_button_color"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/guideBegin"
|
||||
app:layout_constraintTop_toBottomOf="@id/reportDescriptionRemoteInstance" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/checkIsNotifyRemote" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonBack"
|
||||
style="@style/TuskyButton.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/button_back"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/buttonReport" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonReport"
|
||||
style="@style/TuskyButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/action_report"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
72
app/src/main/res/layout/fragment_report_statuses.xml
Normal file
72
app/src/main/res/layout/fragment_report_statuses.xml
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".components.report.fragments.ReportStatusesFragment">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/buttonContinue"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBarTop"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintTop_toTopOf="@id/swipeRefreshLayout" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBarBottom"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintBottom_toBottomOf="@id/swipeRefreshLayout" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBarLoading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/swipeRefreshLayout"
|
||||
app:layout_constraintEnd_toEndOf="@id/swipeRefreshLayout"
|
||||
app:layout_constraintStart_toStartOf="@id/swipeRefreshLayout"
|
||||
app:layout_constraintTop_toTopOf="@id/swipeRefreshLayout" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonCancel"
|
||||
style="@style/TuskyButton.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@android:string/cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/buttonContinue" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonContinue"
|
||||
style="@style/TuskyButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/button_continue"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -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>
|
@ -46,5 +46,4 @@
|
||||
<attr name="status_text_large" format="dimension" />
|
||||
|
||||
<attr name="pollOptionBackgroundColor" format="reference|color" />
|
||||
|
||||
</resources>
|
@ -39,4 +39,5 @@
|
||||
<dimen name="avatar_radius_42dp">5.25dp</dimen> <!-- 1/8 of 42dp -->
|
||||
<dimen name="avatar_radius_36dp">4.5dp</dimen> <!-- 1/8 of 36dp -->
|
||||
<dimen name="avatar_radius_24dp">3dp</dimen> <!-- 1/8 of 24dp -->
|
||||
<dimen name="min_report_button_width">160dp</dimen>
|
||||
</resources>
|
||||
|
@ -471,8 +471,7 @@
|
||||
<string name="poll_info_time_absolute">ends at %s</string>
|
||||
<string name="poll_info_closed">closed</string>
|
||||
<string name="poll_option_format">
|
||||
<!-- 15% vote for this! -->
|
||||
<b>%1$d%%</b> %2$s</string>
|
||||
<!-- 15% vote for this! --> <b>%1$d%%</b> %2$s</string>
|
||||
|
||||
<string name="poll_vote">Vote</string>
|
||||
|
||||
@ -497,4 +496,15 @@
|
||||
<item quantity="other">%d seconds</item>
|
||||
</plurals>
|
||||
|
||||
<string name="button_continue">Continue</string>
|
||||
<string name="button_back">Back</string>
|
||||
<string name="button_done">Done</string>
|
||||
<string name="report_sent_success">Successfully reported @%s</string>
|
||||
<string name="hint_additional_info">Additional comments</string>
|
||||
<string name="report_remote_instance">Forward to %s</string>
|
||||
<string name="failed_report">Failed to report</string>
|
||||
<string name="failed_fetch_statuses">Failed to fetch statuses</string>
|
||||
<string name="report_description_1">The report will be sent to your server moderator. You can provide an explanation of why you are reporting this account below:</string>
|
||||
<string name="report_description_remote_instance">The account is from another server. Send an anonymized copy of the report there as well?</string>
|
||||
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user