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
39 changed files with 2726 additions and 416 deletions
|
@ -6,7 +6,7 @@ apply plugin: 'kotlin-kapt'
|
||||||
def getGitSha = { ->
|
def getGitSha = { ->
|
||||||
def stdout = new ByteArrayOutputStream()
|
def stdout = new ByteArrayOutputStream()
|
||||||
exec {
|
exec {
|
||||||
commandLine 'git', 'rev-parse', '--short' , 'HEAD'
|
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
||||||
standardOutput = stdout
|
standardOutput = stdout
|
||||||
}
|
}
|
||||||
return stdout.toString().trim()
|
return stdout.toString().trim()
|
||||||
|
@ -35,15 +35,15 @@ android {
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles 'proguard-rules.pro'
|
proguardFiles 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
debug { }
|
debug {}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "color"
|
flavorDimensions "color"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
blue { }
|
blue {}
|
||||||
green {
|
green {
|
||||||
applicationIdSuffix ".test"
|
applicationIdSuffix ".test"
|
||||||
versionNameSuffix "-"+getGitSha()
|
versionNameSuffix "-" + getGitSha()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +124,7 @@ dependencies {
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||||
//room
|
//room
|
||||||
implementation 'androidx.room:room-runtime:2.0.0'
|
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'
|
kapt 'androidx.room:room-compiler:2.0.0'
|
||||||
implementation 'androidx.room:room-rxjava2:2.0.0'
|
implementation 'androidx.room:room-rxjava2:2.0.0'
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
@ -153,4 +154,7 @@ dependencies {
|
||||||
//Glide
|
//Glide
|
||||||
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
||||||
implementation 'com.github.bumptech.glide:okhttp3-integration: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=".AccountListActivity" />
|
||||||
<activity android:name=".AboutActivity" />
|
<activity android:name=".AboutActivity" />
|
||||||
<activity android:name=".TabPreferenceActivity" />
|
<activity android:name=".TabPreferenceActivity" />
|
||||||
<activity
|
|
||||||
android:name=".ReportActivity"
|
|
||||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
||||||
android:theme="@style/Base.Theme.AppCompat" />
|
android:theme="@style/Base.Theme.AppCompat" />
|
||||||
|
@ -119,6 +117,8 @@
|
||||||
<activity android:name=".ModalTimelineActivity" />
|
<activity android:name=".ModalTimelineActivity" />
|
||||||
<activity android:name=".LicenseActivity" />
|
<activity android:name=".LicenseActivity" />
|
||||||
<activity android:name=".FiltersActivity" />
|
<activity android:name=".FiltersActivity" />
|
||||||
|
<activity android:name=".components.report.ReportActivity"
|
||||||
|
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||||
|
|
||||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
<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;
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
@ -34,16 +36,23 @@ import dagger.android.DispatchingAndroidInjector;
|
||||||
import dagger.android.support.HasSupportFragmentInjector;
|
import dagger.android.support.HasSupportFragmentInjector;
|
||||||
|
|
||||||
public class ViewTagActivity extends BottomSheetActivity implements HasSupportFragmentInjector {
|
public class ViewTagActivity extends BottomSheetActivity implements HasSupportFragmentInjector {
|
||||||
|
private static final String HASHTAG = "hashtag";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
|
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
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_view_tag);
|
setContentView(R.layout.activity_view_tag);
|
||||||
|
|
||||||
String hashtag = getIntent().getStringExtra("hashtag");
|
String hashtag = getIntent().getStringExtra(HASHTAG);
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
setSupportActionBar(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
|
package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
import com.keylesspalace.tusky.*
|
import com.keylesspalace.tusky.*
|
||||||
|
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
|
@ -71,9 +72,6 @@ abstract class ActivitiesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesSplashActivity(): SplashActivity
|
abstract fun contributesSplashActivity(): SplashActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
|
||||||
abstract fun contributesReportActivity(): ReportActivity
|
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesSavedTootActivity(): SavedTootActivity
|
abstract fun contributesSavedTootActivity(): SavedTootActivity
|
||||||
|
|
||||||
|
@ -92,4 +90,6 @@ abstract class ActivitiesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesFiltersActivity(): FiltersActivity
|
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.*
|
||||||
import com.keylesspalace.tusky.fragment.preference.AccountPreferencesFragment
|
import com.keylesspalace.tusky.fragment.preference.AccountPreferencesFragment
|
||||||
import com.keylesspalace.tusky.fragment.preference.NotificationPreferencesFragment
|
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.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
|
@ -60,4 +63,12 @@ abstract class FragmentBuildersModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun accountInListsFragment(): AccountsInListFragment
|
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.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||||
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
import com.keylesspalace.tusky.viewmodel.*
|
||||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.MapKey
|
import dagger.MapKey
|
||||||
|
@ -61,5 +60,10 @@ abstract class ViewModelModule {
|
||||||
@ViewModelKey(AccountsInListViewModel::class)
|
@ViewModelKey(AccountsInListViewModel::class)
|
||||||
internal abstract fun accountsInListViewModel(viewModel: AccountsInListViewModel): ViewModel
|
internal abstract fun accountsInListViewModel(viewModel: AccountsInListViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(ReportViewModel::class)
|
||||||
|
internal abstract fun reportViewModel(viewModel: ReportViewModel): ViewModel
|
||||||
|
|
||||||
//Add more ViewModels here
|
//Add more ViewModels here
|
||||||
}
|
}
|
|
@ -44,9 +44,9 @@ import com.keylesspalace.tusky.BottomSheetActivity;
|
||||||
import com.keylesspalace.tusky.ComposeActivity;
|
import com.keylesspalace.tusky.ComposeActivity;
|
||||||
import com.keylesspalace.tusky.MainActivity;
|
import com.keylesspalace.tusky.MainActivity;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.ReportActivity;
|
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||||
import com.keylesspalace.tusky.ViewTagActivity;
|
import com.keylesspalace.tusky.ViewTagActivity;
|
||||||
|
import com.keylesspalace.tusky.components.report.ReportActivity;
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
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.entity.Status;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.network.TimelineCases;
|
import com.keylesspalace.tusky.network.TimelineCases;
|
||||||
import com.keylesspalace.tusky.util.HtmlUtils;
|
|
||||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
|
||||||
|
|
||||||
import java.util.LinkedHashSet;
|
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,
|
protected void openReportPage(String accountId, String accountUsername, String statusId,
|
||||||
Spanned statusContent) {
|
Spanned statusContent) {
|
||||||
Intent intent = new Intent(getContext(), ReportActivity.class);
|
startActivity(ReportActivity.getIntent(requireContext(),accountId,accountUsername,statusId,statusContent));
|
||||||
intent.putExtra("account_id", accountId);
|
|
||||||
intent.putExtra("account_username", accountUsername);
|
|
||||||
intent.putExtra("status_id", statusId);
|
|
||||||
intent.putExtra("status_content", HtmlUtils.toHtml(statusContent));
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void showConfirmDeleteDialog(final String id, final int position) {
|
protected void showConfirmDeleteDialog(final String id, final int position) {
|
||||||
|
|
|
@ -385,4 +385,40 @@ public interface MastodonApi {
|
||||||
@Path("id") String id,
|
@Path("id") String id,
|
||||||
@Field("choices[]") List<Integer> choices
|
@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,
|
class Error<T> (override val data: T? = null,
|
||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
var consumed: Boolean = false
|
var consumed: Boolean = false,
|
||||||
|
val cause: Throwable? = null
|
||||||
): Resource<T>(data)
|
): 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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<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_width="match_parent"
|
||||||
android:layout_height="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" />
|
<include layout="@layout/toolbar_basic" />
|
||||||
|
|
||||||
<LinearLayout
|
<com.keylesspalace.tusky.view.NoSwipeViewPager
|
||||||
|
android:id="@+id/wizard"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:overScrollMode="never"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<include layout="@layout/item_status_bottom_sheet" />
|
||||||
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>
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</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"?>
|
<?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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
<TextView
|
<androidx.constraintlayout.widget.Guideline
|
||||||
android:id="@+id/report_status_content"
|
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_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
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:layout_weight="1"
|
||||||
android:padding="8dp"
|
android:textColor="?android:textColorPrimary"
|
||||||
android:textSize="?attr/status_text_medium" />
|
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
|
<CheckBox
|
||||||
android:id="@+id/report_status_check_box"
|
android:id="@+id/statusSelection"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_vertical"
|
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="status_text_large" format="dimension" />
|
||||||
|
|
||||||
<attr name="pollOptionBackgroundColor" format="reference|color" />
|
<attr name="pollOptionBackgroundColor" format="reference|color" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -39,4 +39,5 @@
|
||||||
<dimen name="avatar_radius_42dp">5.25dp</dimen> <!-- 1/8 of 42dp -->
|
<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_36dp">4.5dp</dimen> <!-- 1/8 of 36dp -->
|
||||||
<dimen name="avatar_radius_24dp">3dp</dimen> <!-- 1/8 of 24dp -->
|
<dimen name="avatar_radius_24dp">3dp</dimen> <!-- 1/8 of 24dp -->
|
||||||
|
<dimen name="min_report_button_width">160dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -471,8 +471,7 @@
|
||||||
<string name="poll_info_time_absolute">ends at %s</string>
|
<string name="poll_info_time_absolute">ends at %s</string>
|
||||||
<string name="poll_info_closed">closed</string>
|
<string name="poll_info_closed">closed</string>
|
||||||
<string name="poll_option_format">
|
<string name="poll_option_format">
|
||||||
<!-- 15% vote for this! -->
|
<!-- 15% vote for this! --> <b>%1$d%%</b> %2$s</string>
|
||||||
<b>%1$d%%</b> %2$s</string>
|
|
||||||
|
|
||||||
<string name="poll_vote">Vote</string>
|
<string name="poll_vote">Vote</string>
|
||||||
|
|
||||||
|
@ -497,4 +496,15 @@
|
||||||
<item quantity="other">%d seconds</item>
|
<item quantity="other">%d seconds</item>
|
||||||
</plurals>
|
</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>
|
</resources>
|
||||||
|
|
Loading…
Add table
Reference in a new issue