Commit b5063e3e authored by Thomas's avatar Thomas
Browse files

fixes db migration issue + some other minor bugs + prepares release 1.3.5

parent 8c69adfe
<b>Mastalab is a multi-accounts client for Mastodon</b>
## Mastalab is a multi-accounts client for Mastodon
The number of libraries is minimized and it does not use tracking tools. The source code is free (GPLv3). Any help would be greatly appreciated to fix spelling or for any other suggestions.
**Features**
<b>Multi-accounts management</b>
- Add accounts from different instances
- Switch from one account to another by a simple click
<b>Timelines</b>
- Federated / Local / Home
- Switch from one timeline to another by using the menu or by swiping the screen.
- Clicks on toots display the related conversations (context)
- Clicks on mentioned accounts display details about these accounts
- Clicks on hashtags display toots containing this hashtags
<b>Actions on toots</b>
- Mute an account related to a toot
- Block an account related to a toot
- Report inappropriate toots to administrators
- Add/Remove a toot from favourites
- Boost/Unboost toots
- Copy the content of a toot
- Download media
- Translation of toots by a simple click (via the Yandex API)
<b>Write a toot</b>
- Add media
- Change the visibility of the toot
- Mention accounts in toots with autocompletion (@ + 2 characters)
- Mark the content as sensitive
- Add spoilers
<b>Interaction with accounts</b>
- Follow/Unfollow/Block/Unblock/Mute/Unmute
- Display details of accounts
- Authorize/Reject follow requests (for locked accounts)
<b>Searches</b>
- A top bar allows to make researches for accounts/tags/toots
- A click on a tag displays toots containing this tag
<b>Network optimization</b>
- Load of media: Automatic/WIFI only/Ask
- Customization of the number of toots/accounts per load
<b>Notifications</b>
- Notifications for new toots on the home page (could be disabled in settings)
- Notifications for new events (could be disabled or filtered in settings)
<b>Built-in browser</b>
- Full screen videos
- Disable JavaScript (default: enabled)
- Disable third-party cookies (default: disabled)
- Disable the built-in browser in settings
### Features
**Multi-accounts management**
* Add accounts from different instances
* Switch from one account to another by a simple click
**Timelines**
* Federated / Local / Home
* Switch from one timeline to another by using the menu or by swiping the screen.
* Clicks on toots display the related conversations (context)
* Clicks on mentioned accounts display details about these accounts
* Clicks on hashtags display toots containing this hashtags
**Actions on toots**
* Mute an account related to a toot
* Block an account related to a toot
* Report inappropriate toots to administrators
* Add/Remove a toot from favourites
* Boost/Unboost toots
* Copy the content of a toot
* Download media
* Translation of toots by a simple click (via the Yandex API)
**Write a toot**
* Add media
* Change the visibility of the toot
* Mention accounts in toots with autocompletion (@ + 2 characters)
* Mark the content as sensitive
* Add spoilers
* Toots which have not been sent are saved (drafts) - can be disabled in settings
* Drafts can be edited/deleted/scheduled
**Scheduled toots**
* Can be edited/deleted/scheduled at another date as long as they have not been sent.
**Interaction with accounts**
* Follow/Unfollow/Block/Unblock/Mute/Unmute
* Display details of accounts
* Authorize/Reject follow requests (for locked accounts)
**Searches**
* A top bar allows to make researches for accounts/tags/toots
* A click on a tag displays toots containing this tag
**Network optimization**
* Load of media: Automatic/WIFI only/Ask
* Customization of the number of toots/accounts per load
**Notifications**
* Notifications for new toots on the home page (could be disabled in settings)
* Notifications for new events (could be disabled or filtered in settings)
**Built-in browser**
* Full screen videos
* Disable JavaScript (default: enabled)
* Disable third-party cookies (default: disabled)
* Disable the built-in browser in settings
Developer: [@tschneider](https://mastodon.etalab.gouv.fr/@tschneider)
......
......@@ -7,8 +7,8 @@ android {
applicationId "fr.gouv.etalab.mastodon"
minSdkVersion 15
targetSdkVersion 25
versionCode 31
versionName "1.3.4"
versionCode 32
versionName "1.3.5"
}
buildTypes {
release {
......
......@@ -91,7 +91,8 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi
private BroadcastReceiver hide_header;
private TextView account_note;
private String userId;
private static boolean isHiddingShowing = false;
private boolean isHiddingShowing = false;
private static int instanceValue = 0;
public enum action{
FOLLOW,
......@@ -113,7 +114,7 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi
setTheme(R.style.AppThemeDark);
}
setContentView(R.layout.activity_show_account);
instanceValue += 1;
imageLoader = ImageLoader.getInstance();
statuses = new ArrayList<>();
boolean isOnWifi = Helper.isOnWIFI(getApplicationContext());
......@@ -220,12 +221,12 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi
public void run() {
isHiddingShowing = false;
}
}, 1000);
}, 500);
}
}
};
LocalBroadcastManager.getInstance(this).registerReceiver(hide_header, new IntentFilter(Helper.HEADER_ACCOUNT));
LocalBroadcastManager.getInstance(this).registerReceiver(hide_header, new IntentFilter(Helper.HEADER_ACCOUNT+String.valueOf(instanceValue)));
//Follow button
account_follow.setOnClickListener(new View.OnClickListener() {
......@@ -380,6 +381,7 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi
bundle.putSerializable("type", RetrieveFeedsAsyncTask.Type.USER);
bundle.putString("targetedId", accountId);
bundle.putBoolean("hideHeader",true);
bundle.putString("hideHeaderValue",String.valueOf(instanceValue));
displayStatusFragment.setArguments(bundle);
return displayStatusFragment;
case 1:
......@@ -387,6 +389,7 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi
bundle.putSerializable("type", RetrieveAccountsAsyncTask.Type.FOLLOWING);
bundle.putString("targetedId", accountId);
bundle.putBoolean("hideHeader",true);
bundle.putString("hideHeaderValue",String.valueOf(instanceValue));
displayAccountsFragment.setArguments(bundle);
return displayAccountsFragment;
case 2:
......@@ -394,6 +397,7 @@ public class ShowAccountActivity extends AppCompatActivity implements OnPostActi
bundle.putSerializable("type", RetrieveAccountsAsyncTask.Type.FOLLOWERS);
bundle.putString("targetedId", accountId);
bundle.putBoolean("hideHeader",true);
bundle.putString("hideHeaderValue",String.valueOf(instanceValue));
displayAccountsFragment.setArguments(bundle);
return displayAccountsFragment;
}
......
......@@ -14,10 +14,13 @@ package fr.gouv.etalab.mastodon.drawers;
* You should have received a copy of the GNU General Public License along with Thomas Schneider; if not,
* see <http://www.gnu.org/licenses>. */
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
......@@ -30,16 +33,22 @@ import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.display.SimpleBitmapDisplayer;
import java.util.ArrayList;
import java.util.List;
import fr.gouv.etalab.mastodon.activities.ShowAccountActivity;
import fr.gouv.etalab.mastodon.activities.ShowConversationActivity;
import fr.gouv.etalab.mastodon.activities.TootActivity;
import fr.gouv.etalab.mastodon.asynctasks.PostActionAsyncTask;
import fr.gouv.etalab.mastodon.client.API;
import fr.gouv.etalab.mastodon.client.Entities.Error;
import fr.gouv.etalab.mastodon.interfaces.OnPostActionInterface;
import mastodon.etalab.gouv.fr.mastodon.R;
import fr.gouv.etalab.mastodon.client.Entities.Notification;
import fr.gouv.etalab.mastodon.client.Entities.Status;
......@@ -52,19 +61,23 @@ import static fr.gouv.etalab.mastodon.helper.Helper.changeDrawableColor;
* Created by Thomas on 24/04/2017.
* Adapter for Status
*/
public class NotificationsListAdapter extends BaseAdapter {
public class NotificationsListAdapter extends BaseAdapter implements OnPostActionInterface {
private Context context;
private List<Notification> notifications;
private LayoutInflater layoutInflater;
private ImageLoader imageLoader;
private DisplayImageOptions options;
private final int REBLOG = 1;
private final int FAVOURITE = 2;
private NotificationsListAdapter notificationsListAdapter;
public NotificationsListAdapter(Context context, List<Notification> notifications){
this.context = context;
this.notifications = notifications;
layoutInflater = LayoutInflater.from(this.context);
imageLoader = ImageLoader.getInstance();
notificationsListAdapter = this;
options = new DisplayImageOptions.Builder().displayer(new SimpleBitmapDisplayer()).cacheInMemory(false)
.cacheOnDisk(true).resetViewBeforeLoading(true).build();
}
......@@ -130,7 +143,7 @@ public class NotificationsListAdapter extends BaseAdapter {
}
holder.notification_type.setText(typeString);
final SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
final Status status = notification.getStatus();
if( status != null ){
......@@ -138,7 +151,6 @@ public class NotificationsListAdapter extends BaseAdapter {
holder.status_document_container.setVisibility(View.GONE);
else
holder.status_document_container.setVisibility(View.VISIBLE);
if( (status.getIn_reply_to_account_id() != null && !status.getIn_reply_to_account_id().equals("null")) || (status.getIn_reply_to_id() != null && !status.getIn_reply_to_id().equals("null")) ){
Drawable img = ContextCompat.getDrawable(context, R.drawable.ic_reply);
img.setBounds(0,0,(int) (20 * scale + 0.5f),(int) (15 * scale + 0.5f));
......@@ -161,7 +173,7 @@ public class NotificationsListAdapter extends BaseAdapter {
holder.status_date.setText(Helper.dateDiff(context, status.getCreated_at()));
//Manages theme for icon colors
final SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
int theme = sharedpreferences.getInt(Helper.SET_THEME, Helper.THEME_DARK);
if( theme == Helper.THEME_DARK){
changeDrawableColor(context, R.drawable.ic_reply,R.color.dark_text);
......@@ -171,6 +183,7 @@ public class NotificationsListAdapter extends BaseAdapter {
changeDrawableColor(context, R.drawable.ic_action_lock_closed,R.color.dark_text);
changeDrawableColor(context, R.drawable.ic_local_post_office,R.color.dark_text);
changeDrawableColor(context, R.drawable.ic_retweet_black,R.color.dark_text);
changeDrawableColor(context, R.drawable.ic_retweet,R.color.dark_text);
changeDrawableColor(context, R.drawable.ic_fav_black,R.color.dark_text);
changeDrawableColor(context, R.drawable.ic_photo,R.color.dark_text);
changeDrawableColor(context, R.drawable.ic_remove_red_eye,R.color.dark_text);
......@@ -182,6 +195,7 @@ public class NotificationsListAdapter extends BaseAdapter {
changeDrawableColor(context, R.drawable.ic_action_lock_closed,R.color.black);
changeDrawableColor(context, R.drawable.ic_local_post_office,R.color.black);
changeDrawableColor(context, R.drawable.ic_retweet_black,R.color.black);
changeDrawableColor(context, R.drawable.ic_retweet,R.color.black);
changeDrawableColor(context, R.drawable.ic_fav_black,R.color.black);
changeDrawableColor(context, R.drawable.ic_photo,R.color.black);
changeDrawableColor(context, R.drawable.ic_remove_red_eye,R.color.black);
......@@ -214,6 +228,18 @@ public class NotificationsListAdapter extends BaseAdapter {
holder.status_privacy.setImageResource(R.drawable.ic_local_post_office);
break;
}
switch (status.getVisibility()){
case "direct":
case "private":
holder.status_reblog_count.setVisibility(View.GONE);
break;
case "public":
case "unlisted":
holder.status_reblog_count.setVisibility(View.VISIBLE);
break;
default:
holder.status_reblog_count.setVisibility(View.VISIBLE);
}
Drawable imgFav, imgReblog;
if( status.isFavourited())
imgFav = ContextCompat.getDrawable(context, R.drawable.ic_fav_yellow);
......@@ -234,6 +260,28 @@ public class NotificationsListAdapter extends BaseAdapter {
}
holder.status_favorite_count.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean confirmation = sharedpreferences.getBoolean(Helper.SET_NOTIF_VALIDATION, true);
if( confirmation )
displayConfirmationDialog(FAVOURITE,status);
else
favouriteAction(status);
}
});
holder.status_reblog_count.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean confirmation = sharedpreferences.getBoolean(Helper.SET_NOTIF_VALIDATION, true);
if( confirmation )
displayConfirmationDialog(REBLOG,status);
else
reblogAction(status);
}
});
holder.notification_account_profile.setOnClickListener(new View.OnClickListener() {
@Override
......@@ -245,6 +293,7 @@ public class NotificationsListAdapter extends BaseAdapter {
context.startActivity(intent);
}
});
holder.status_reply.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
......@@ -256,6 +305,7 @@ public class NotificationsListAdapter extends BaseAdapter {
}
});
holder.notification_account_displayname.setText(Helper.shortnameToUnicode(notification.getAccount().getDisplay_name(), true));
holder.notification_account_username.setText( String.format("@%s",notification.getAccount().getUsername()));
//Profile picture
......@@ -263,6 +313,94 @@ public class NotificationsListAdapter extends BaseAdapter {
return convertView;
}
/**
* Display a validation message
* @param action int
* @param status Status
*/
private void displayConfirmationDialog(final int action, final Status status){
String title = null;
if( action == FAVOURITE){
if( status.isFavourited())
title = context.getString(R.string.favourite_remove);
else
title = context.getString(R.string.favourite_add);
}else if( action == REBLOG ){
if( status.isReblogged())
title = context.getString(R.string.reblog_remove);
else
title = context.getString(R.string.reblog_add);
}
AlertDialog.Builder builder = new AlertDialog.Builder(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
builder.setMessage(Html.fromHtml(status.getContent(), Html.FROM_HTML_MODE_COMPACT));
else
//noinspection deprecation
builder.setMessage(Html.fromHtml(status.getContent()));
builder.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(title)
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if( action == REBLOG)
reblogAction(status);
else if( action == FAVOURITE)
favouriteAction(status);
dialog.dismiss();
}
})
.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.show();
}
/**
* Favourites/Unfavourites a status
* @param status Status
*/
private void favouriteAction(Status status){
if( status.isFavourited()){
new PostActionAsyncTask(context, API.StatusAction.UNFAVOURITE, status.getId(), NotificationsListAdapter.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
status.setFavourited(false);
}else{
new PostActionAsyncTask(context, API.StatusAction.FAVOURITE, status.getId(), NotificationsListAdapter.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
status.setFavourited(true);
}
notificationsListAdapter.notifyDataSetChanged();
}
/**
* Reblog/Unreblog a status
* @param status Status
*/
private void reblogAction(Status status){
if( status.isReblogged()){
new PostActionAsyncTask(context, API.StatusAction.UNREBLOG, status.getId(), NotificationsListAdapter.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
status.setReblogged(false);
}else{
new PostActionAsyncTask(context, API.StatusAction.REBLOG, status.getId(), NotificationsListAdapter.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
status.setReblogged(true);
}
notificationsListAdapter.notifyDataSetChanged();
}
@Override
public void onPostAction(int statusCode, API.StatusAction statusAction, String userId, Error error) {
if( error != null){
final SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
boolean show_error_messages = sharedpreferences.getBoolean(Helper.SET_SHOW_ERROR_MESSAGES, true);
if( show_error_messages)
Toast.makeText(context, error.getError(),Toast.LENGTH_LONG).show();
}
}
private class ViewHolder {
TextView notification_status_content;
......
......@@ -478,9 +478,21 @@ public class StatusListAdapter extends BaseAdapter implements OnPostActionInterf
if( confirmation )
displayConfirmationDialog(REBLOG,status);
else
favouriteAction(status);
reblogAction(status);
}
});
switch (status.getVisibility()){
case "direct":
case "private":
holder.status_reblog_count.setVisibility(View.GONE);
break;
case "public":
case "unlisted":
holder.status_reblog_count.setVisibility(View.VISIBLE);
break;
default:
holder.status_reblog_count.setVisibility(View.VISIBLE);
}
holder.status_more.setOnClickListener(new View.OnClickListener() {
@Override
......
......@@ -22,6 +22,7 @@ import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
......@@ -61,7 +62,7 @@ public class DisplayAccountsFragment extends Fragment implements OnRetrieveAccou
private String targetedId;
private boolean swiped;
private ListView lv_accounts;
private String instanceValue;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
......@@ -77,6 +78,7 @@ public class DisplayAccountsFragment extends Fragment implements OnRetrieveAccou
type = (RetrieveAccountsAsyncTask.Type) bundle.get("type");
targetedId = bundle.getString("targetedId", null);
hideHeader = bundle.getBoolean("hideHeader", false);
instanceValue = bundle.getString("hideHeaderValue", null);
if( bundle.containsKey("accounts")){
ArrayList<Parcelable> accountsReceived = bundle.getParcelableArrayList("accounts");
assert accountsReceived != null;
......@@ -117,19 +119,15 @@ public class DisplayAccountsFragment extends Fragment implements OnRetrieveAccou
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if( firstVisibleItem == 0) {
Intent intent = new Intent(Helper.HEADER_ACCOUNT);
intent.putExtra("hide", false);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}else if (view.getId() == lv_accounts.getId() && totalItemCount > visibleItemCount) {
if (view.getId() == lv_accounts.getId() && totalItemCount > visibleItemCount) {
final int currentFirstVisibleItem = lv_accounts.getFirstVisiblePosition();
if (currentFirstVisibleItem > lastFirstVisibleItem) {
Intent intent = new Intent(Helper.HEADER_ACCOUNT);
Intent intent = new Intent(Helper.HEADER_ACCOUNT + instanceValue);
intent.putExtra("hide", true);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
} else if (currentFirstVisibleItem < lastFirstVisibleItem) {
Intent intent = new Intent(Helper.HEADER_ACCOUNT);
Intent intent = new Intent(Helper.HEADER_ACCOUNT + instanceValue);
intent.putExtra("hide", false);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
......
......@@ -69,6 +69,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
private ListView lv_status;
private boolean isOnWifi;
private int behaviorWithAttachments;
private String instanceValue;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
......@@ -84,6 +85,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
targetedId = bundle.getString("targetedId", null);
tag = bundle.getString("tag", null);
hideHeader = bundle.getBoolean("hideHeader", false);
instanceValue = bundle.getString("hideHeaderValue", null);
if( bundle.containsKey("statuses")){
ArrayList<Parcelable> statusesReceived = bundle.getParcelableArrayList("statuses");
assert statusesReceived != null;
......@@ -126,19 +128,15 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if( firstVisibleItem == 0) {
Intent intent = new Intent(Helper.HEADER_ACCOUNT);
intent.putExtra("hide", false);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}else if (view.getId() == lv_status.getId() && totalItemCount > visibleItemCount) {
if (view.getId() == lv_status.getId() && totalItemCount > visibleItemCount) {
final int currentFirstVisibleItem = lv_status.getFirstVisiblePosition();
if (currentFirstVisibleItem > lastFirstVisibleItem) {
Intent intent = new Intent(Helper.HEADER_ACCOUNT);
Intent intent = new Intent(Helper.HEADER_ACCOUNT+instanceValue);
intent.putExtra("hide", true);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
} else if (currentFirstVisibleItem < lastFirstVisibleItem) {
Intent intent = new Intent(Helper.HEADER_ACCOUNT);
Intent intent = new Intent(Helper.HEADER_ACCOUNT+instanceValue);
intent.putExtra("hide", false);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
......
......@@ -82,7 +82,7 @@ public class Sqlite extends SQLiteOpenHelper {
private static final String CREATE_TABLE_STATUSES_STORED = "CREATE TABLE " + TABLE_STATUSES_STORED + " ("
+ COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ COL_USER_ID + " TEXT NOT NULL, " + COL_INSTANCE + " TEXT NOT NULL, "
+ COL_STATUS_SERIALIZED + " TEXT NOT NULL, " + COL_DATE_CREATION + " TEXT NOT NULL, "
+ COL_STATUS_SERIALIZED + " TEXT NOT NULL, " + COL_STATUS_REPLY_SERIALIZED + " TEXT, " + COL_DATE_CREATION + " TEXT NOT NULL, "
+ COL_IS_SCHEDULED + " INTEGER NOT NULL, " + COL_DATE_SCHEDULED + " TEXT, "
+ COL_SENT + " INTEGER NOT NULL, " + COL_DATE_SENT + " TEXT)";
......@@ -112,7 +112,8 @@ public class Sqlite extends SQLiteOpenHelper {
case 1:
db.execSQL(CREATE_TABLE_STATUSES_STORED);
case 2:
db.execSQL("ALTER TABLE " + TABLE_STATUSES_STORED + " ADD COLUMN " + COL_STATUS_REPLY_SERIALIZED + " TEXT");
db.execSQL("DROP TABLE IF EXISTS " + TABLE_STATUSES_STORED);
db.execSQL(CREATE_TABLE_STATUSES_STORED);
default:
break;
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment