diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/activities/BaseMainActivity.java b/app/src/main/java/fr/gouv/etalab/mastodon/activities/BaseMainActivity.java index cfc6346f2..229221bd8 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/activities/BaseMainActivity.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/activities/BaseMainActivity.java @@ -409,8 +409,9 @@ public abstract class BaseMainActivity extends BaseActivity SubMenu submMastodon = popup.getMenu().findItem(R.id.action_show_mastodon).getSubMenu(); SubMenu submPeertube = popup.getMenu().findItem(R.id.action_show_peertube).getSubMenu(); SubMenu submPixelfed = popup.getMenu().findItem(R.id.action_show_pixelfed).getSubMenu(); + SubMenu submMisskey = popup.getMenu().findItem(R.id.action_show_misskey).getSubMenu(); SubMenu submChannel = popup.getMenu().findItem(R.id.action_show_channel).getSubMenu(); - int i = 0, j = 0 , k = 0; + int i = 0, j = 0 , k = 0, l = 0 , m = 0; for (RemoteInstance remoteInstance : remoteInstances) { if (remoteInstance.getType() == null || remoteInstance.getType().equals("MASTODON")) { MenuItem itemPlaceHolder = submMastodon.findItem(R.id.mastodon_instances); @@ -506,11 +507,42 @@ public abstract class BaseMainActivity extends BaseActivity }); j++; } + if (remoteInstance.getType() == null || remoteInstance.getType().equals("MISSKEY")) { + MenuItem itemPlaceHolder = submPixelfed.findItem(R.id.misskey_instance); + if( itemPlaceHolder != null) + itemPlaceHolder.setVisible(false); + MenuItem item = submMisskey.add(0, l, Menu.NONE, remoteInstance.getHost()); + item.setIcon(R.drawable.misskey); + item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + DisplayStatusFragment statusFragment; + Bundle bundle = new Bundle(); + statusFragment = new DisplayStatusFragment(); + bundle.putSerializable("type", RetrieveFeedsAsyncTask.Type.REMOTE_INSTANCE); + bundle.putString("remote_instance", remoteInstance.getHost()); + statusFragment.setArguments(bundle); + String fragmentTag = "REMOTE_INSTANCE"; + instance_id = remoteInstance.getDbID(); + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.main_app_container, statusFragment, fragmentTag).commit(); + main_app_container.setVisibility(View.VISIBLE); + viewPager.setVisibility(View.GONE); + tabLayout.setVisibility(View.GONE); + toolbarTitle.setVisibility(View.VISIBLE); + delete_instance.setVisibility(View.VISIBLE); + toolbarTitle.setText(remoteInstance.getHost()); + return false; + } + }); + l++; + } if (remoteInstance.getType() == null || remoteInstance.getType().equals("PEERTUBE")) { MenuItem itemPlaceHolder = submPeertube.findItem(R.id.peertube_instances); if( itemPlaceHolder != null) itemPlaceHolder.setVisible(false); - MenuItem item = submPeertube.add(0, j, Menu.NONE, remoteInstance.getHost()); + MenuItem item = submPeertube.add(0, m, Menu.NONE, remoteInstance.getHost()); item.setIcon(R.drawable.peertube_icon); item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override @@ -535,7 +567,7 @@ public abstract class BaseMainActivity extends BaseActivity return false; } }); - j++; + m++; } } } @@ -568,6 +600,8 @@ public abstract class BaseMainActivity extends BaseActivity new HttpsConnection(BaseMainActivity.this).get("https://" + instanceName + "/api/v1/videos/", 10, null, null); else if( radioGroup.getCheckedRadioButtonId() == R.id.pixelfed_instance) { new HttpsConnection(BaseMainActivity.this).get("https://" + instanceName + "/api/v1/timelines/public", 10, null, null); + }else if( radioGroup.getCheckedRadioButtonId() == R.id.misskey_instance) { + new HttpsConnection(BaseMainActivity.this).post("https://" + instanceName + "/api/notes/local-timeline", 10, null, null); } runOnUiThread(new Runnable() { public void run() { @@ -579,6 +613,8 @@ public abstract class BaseMainActivity extends BaseActivity new InstancesDAO(BaseMainActivity.this, db).insertInstance(instanceName, "PEERTUBE"); else if( radioGroup.getCheckedRadioButtonId() == R.id.pixelfed_instance) new InstancesDAO(BaseMainActivity.this, db).insertInstance(instanceName, "PIXELFED"); + else if( radioGroup.getCheckedRadioButtonId() == R.id.misskey_instance) + new InstancesDAO(BaseMainActivity.this, db).insertInstance(instanceName, "MISSKEY"); DisplayStatusFragment statusFragment; Bundle bundle = new Bundle(); statusFragment = new DisplayStatusFragment(); diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/RetrieveFeedsAsyncTask.java b/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/RetrieveFeedsAsyncTask.java index 40f57e2e7..0689281fb 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/RetrieveFeedsAsyncTask.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/asynctasks/RetrieveFeedsAsyncTask.java @@ -176,7 +176,15 @@ public class RetrieveFeedsAsyncTask extends AsyncTask { status.setType(action); } } - }else { + }else if(remoteInstanceObj != null && remoteInstanceObj.size() > 0 && remoteInstanceObj.get(0).getType().equals("MISSKEY")){ + apiResponse = api.getMisskey(this.instanceName, max_id); + List statusesTemp = apiResponse.getStatuses(); + if( statusesTemp != null){ + for(fr.gouv.etalab.mastodon.client.Entities.Status status: statusesTemp){ + status.setType(action); + } + } + } else { apiResponse = api.getPeertube(this.instanceName, max_id); } } diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/client/API.java b/app/src/main/java/fr/gouv/etalab/mastodon/client/API.java index 730d09da2..cbf5f817a 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/client/API.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/client/API.java @@ -17,6 +17,7 @@ package fr.gouv.etalab.mastodon.client; import android.content.Context; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; +import android.util.Log; import org.json.JSONArray; import org.json.JSONException; @@ -1039,6 +1040,39 @@ public class API { apiResponse.setHowToVideos(howToVideos); return apiResponse; } + + + /** + * Retrieves Peertube videos from an instance *synchronously* + * @return APIResponse + */ + public APIResponse getMisskey(String instance, String max_id) { + + + HashMap params = new HashMap<>(); + if( max_id != null) + params.put("untilId",max_id); + try { + statuses = new ArrayList<>(); + HttpsConnection httpsConnection = new HttpsConnection(context); + String response = httpsConnection.post(String.format("https://"+instance+"/api/notes/local-timeline", max_id), 60, params, null); + apiResponse.setSince_id(httpsConnection.getSince_id()); + apiResponse.setMax_id(httpsConnection.getMax_id()); + statuses = parseNotes(context, instance, new JSONArray(response)); + } catch (HttpsConnection.HttpsConnectionException e) { + setError(e.getStatusCode(), e); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (KeyManagementException e) { + e.printStackTrace(); + } catch (JSONException e) { + e.printStackTrace(); + } + apiResponse.setStatuses(statuses); + return apiResponse; + } /** * Retrieves public timeline for the account *synchronously* * @param local boolean only local timeline @@ -3134,6 +3168,150 @@ public class API { return status; } + + /** + * Parse json response for several notes (Misskey) + * @param jsonArray JSONArray + * @return List + */ + public static List parseNotes(Context context, String instance, JSONArray jsonArray){ + + List statuses = new ArrayList<>(); + try { + int i = 0; + while (i < jsonArray.length() ){ + + JSONObject resobj = jsonArray.getJSONObject(i); + Status status = parseNotes(context, instance, resobj); + i++; + statuses.add(status); + } + + } catch (JSONException e) { + e.printStackTrace(); + } + return statuses; + } + + /** + * Parse json response for unique note (misskey) + * @param resobj JSONObject + * @return Status + */ + @SuppressWarnings("InfiniteRecursion") + public static Status parseNotes(Context context, String instance, JSONObject resobj){ + Status status = new Status(); + try { + status.setId(resobj.get("id").toString()); + status.setUri("https://" + instance + "/notes/" + resobj.get("id").toString()); + status.setCreated_at(Helper.mstStringToDate(context, resobj.get("createdAt").toString())); + status.setIn_reply_to_id(resobj.get("replyId").toString()); + status.setSensitive(false); + if(resobj.get("cw") != null && !resobj.get("cw").toString().equals("null")) + status.setSpoiler_text(resobj.get("cw").toString()); + try { + status.setVisibility(resobj.get("visibility").toString()); + }catch (Exception e){status.setVisibility("public"); e.printStackTrace();} + status.setUrl("https://" + instance + "/notes/" + resobj.get("id").toString()); + //Retrieves attachments + JSONArray arrayAttachement = resobj.getJSONArray("media"); + ArrayList attachments = new ArrayList<>(); + if( arrayAttachement != null){ + for(int j = 0 ; j < arrayAttachement.length() ; j++){ + JSONObject attObj = arrayAttachement.getJSONObject(j); + Attachment attachment = new Attachment(); + attachment.setId(attObj.get("id").toString()); + attachment.setPreview_url(attObj.get("thumbnailUrl").toString()); + attachment.setRemote_url(attObj.get("url").toString()); + if( attObj.get("type").toString().contains("/")){ + attachment.setType(attObj.get("type").toString().split("/")[0]); + }else + attachment.setType(attObj.get("type").toString()); + attachment.setText_url(attObj.get("url").toString()); + attachment.setUrl(attObj.get("url").toString()); + if(attObj.get("isSensitive").toString().equals("true")){ + status.setSensitive(true); + } + try { + attachment.setDescription(attObj.get("comment").toString()); + }catch (JSONException ignore){ignore.printStackTrace();} + attachments.add(attachment); + } + } + try { + status.setCard(parseCardResponse(resobj.getJSONObject("card"))); + }catch (Exception e){status.setCard(null);} + + status.setMedia_attachments(attachments); + //Retrieves mentions + List mentions = new ArrayList<>(); + + status.setAccount(parseMisskeyAccountResponse(context, instance, resobj.getJSONObject("user"))); + status.setContent(resobj.get("text").toString()); + try{ + status.setReplies_count(Integer.valueOf(resobj.get("repliesCount").toString())); + }catch (Exception e){ + status.setReplies_count(-1); + } + try { + status.setFavourited(Boolean.valueOf(resobj.get("isFavorited").toString())); + }catch (Exception e){ + status.setFavourited(false); + } + try{ + if(resobj.getJSONObject("renoteId") != null && !resobj.getJSONObject("renoteId").toString().equals("null")) + status.setReblog(parseStatuses(context, resobj.getJSONObject("renote"))); + }catch (Exception ignored){} + + status.setMentions(mentions); + //Retrieves tags + List tags = new ArrayList<>(); + JSONArray arrayTag = resobj.getJSONArray("tags"); + if( arrayTag != null){ + for(int j = 0 ; j < arrayTag.length() ; j++){ + JSONObject tagObj = arrayTag.getJSONObject(j); + Tag tag = new Tag(); + tag.setName(tagObj.get("name").toString()); + tag.setUrl(tagObj.get("url").toString()); + tags.add(tag); + } + } + status.setTags(tags); + + //Retrieves emjis + List emojiList = new ArrayList<>(); + try { + JSONArray emojisTag = resobj.getJSONArray("emojis"); + if( emojisTag != null){ + for(int j = 0 ; j < emojisTag.length() ; j++){ + JSONObject emojisObj = emojisTag.getJSONObject(j); + Emojis emojis = parseEmojis(emojisObj); + emojiList.add(emojis); + } + } + status.setEmojis(emojiList); + }catch (Exception e){ + status.setEmojis(new ArrayList<>()); + } + + //Retrieve Application + Application application = new Application(); + try { + if(resobj.getJSONObject("application") != null){ + application.setName(resobj.getJSONObject("application").getString("name")); + application.setWebsite(resobj.getJSONObject("application").getString("website")); + } + }catch (Exception e){ + application = new Application(); + } + status.setApplication(application); + + + } catch (JSONException ignored) {} catch (ParseException e) { + e.printStackTrace(); + } + return status; + } /** * Parse json response an unique instance * @param resobj JSONObject @@ -3421,6 +3599,56 @@ public class API { return account; } + + /** + * Parse json response an unique account + * @param resobj JSONObject + * @return Account + */ + @SuppressWarnings("InfiniteRecursion") + private static Account parseMisskeyAccountResponse(Context context, String instance, JSONObject resobj){ + + Log.v(Helper.TAG,"resobj= " + resobj); + Account account = new Account(); + try { + account.setId(resobj.get("id").toString()); + account.setUsername(resobj.get("username").toString()); + String host = resobj.get("host").toString(); + String acct; + if( host == null || host.equals("null")) + acct = resobj.get("username").toString(); + else + acct = resobj.get("username").toString() + "@" + host; + account.setAcct(acct); + account.setDisplay_name(resobj.get("name").toString()); + account.setCreated_at(new Date()); + + account.setUrl("https://" + instance + "/@"+account.getUsername()); + account.setAvatar(resobj.get("avatarUrl").toString()); + account.setAvatar_static(resobj.get("avatarUrl").toString()); + try { + account.setBot(Boolean.parseBoolean(resobj.get("isBot").toString())); + }catch (Exception e){ + account.setBot(false); + } + //Retrieves emjis + List emojiList = new ArrayList<>(); + try { + JSONArray emojisTag = resobj.getJSONArray("emojis"); + if( emojisTag != null){ + for(int j = 0 ; j < emojisTag.length() ; j++){ + JSONObject emojisObj = emojisTag.getJSONObject(j); + Emojis emojis = parseEmojis(emojisObj); + emojiList.add(emojis); + } + } + account.setEmojis(emojiList); + }catch (Exception e){ + account.setEmojis(new ArrayList<>()); + } + } catch (JSONException ignored) {} + return account; + } /** * Parse json response for list of accounts * @param jsonArray JSONArray diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/drawers/StatusListAdapter.java b/app/src/main/java/fr/gouv/etalab/mastodon/drawers/StatusListAdapter.java index bdf4a1dc0..d2bbbf1ea 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/drawers/StatusListAdapter.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/drawers/StatusListAdapter.java @@ -437,7 +437,7 @@ public class StatusListAdapter extends RecyclerView.Adapter implements OnPostAct boolean isCompactMode = sharedpreferences.getBoolean(Helper.SET_COMPACT_MODE, false); if( type == RetrieveFeedsAsyncTask.Type.CONTEXT && position == conversationPosition) return FOCUSED_STATUS; - else if( !Helper.filterToots(context, statuses.get(position), timedMute, type)) + else if( type != RetrieveFeedsAsyncTask.Type.REMOTE_INSTANCE && !Helper.filterToots(context, statuses.get(position), timedMute, type)) return HIDDEN_STATUS; else return isCompactMode?COMPACT_STATUS:DISPLAYED_STATUS; @@ -1091,8 +1091,8 @@ public class StatusListAdapter extends RecyclerView.Adapter implements OnPostAct } - - holder.status_mention_spoiler.setText(Helper.makeMentionsClick(context,status.getMentions()), TextView.BufferType.SPANNABLE); + if( status.getMentions() != null) + holder.status_mention_spoiler.setText(Helper.makeMentionsClick(context,status.getMentions()), TextView.BufferType.SPANNABLE); holder.status_mention_spoiler.setMovementMethod(LinkMovementMethod.getInstance()); if( getItemViewType(viewHolder.getAdapterPosition()) != COMPACT_STATUS ) { diff --git a/app/src/main/java/fr/gouv/etalab/mastodon/fragments/DisplayStatusFragment.java b/app/src/main/java/fr/gouv/etalab/mastodon/fragments/DisplayStatusFragment.java index 352c81c93..7281c6c71 100644 --- a/app/src/main/java/fr/gouv/etalab/mastodon/fragments/DisplayStatusFragment.java +++ b/app/src/main/java/fr/gouv/etalab/mastodon/fragments/DisplayStatusFragment.java @@ -179,7 +179,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn statusListAdapter = new StatusListAdapter(context, tagTimeline, targetedId, isOnWifi, this.statuses); lv_status.setAdapter(statusListAdapter); } - }else if( search_peertube == null && (instanceType == null || instanceType.equals("MASTODON") || instanceType.equals("PIXELFED"))) { + }else if( search_peertube == null && (instanceType == null || instanceType.equals("MASTODON") || instanceType.equals("PIXELFED") || instanceType.equals("MISSKEY"))) { BaseMainActivity.displayPeertube = null; if( instanceType != null && instanceType.equals("PIXELFED")) type = RetrieveFeedsAsyncTask.Type.PIXELFED; diff --git a/app/src/main/res/drawable-hdpi/misskey.png b/app/src/main/res/drawable-hdpi/misskey.png new file mode 100644 index 000000000..2961e94fa Binary files /dev/null and b/app/src/main/res/drawable-hdpi/misskey.png differ diff --git a/app/src/main/res/drawable-ldpi/misskey.png b/app/src/main/res/drawable-ldpi/misskey.png new file mode 100644 index 000000000..a06c29370 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/misskey.png differ diff --git a/app/src/main/res/drawable-mdpi/misskey.png b/app/src/main/res/drawable-mdpi/misskey.png new file mode 100644 index 000000000..1e1192d85 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/misskey.png differ diff --git a/app/src/main/res/drawable-xhdpi/misskey.png b/app/src/main/res/drawable-xhdpi/misskey.png new file mode 100644 index 000000000..0d36e8cb0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/misskey.png differ diff --git a/app/src/main/res/drawable-xxhdpi/misskey.png b/app/src/main/res/drawable-xxhdpi/misskey.png new file mode 100644 index 000000000..5218d7f4b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/misskey.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/misskey.png b/app/src/main/res/drawable-xxxhdpi/misskey.png new file mode 100644 index 000000000..d1863aab3 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/misskey.png differ diff --git a/app/src/main/res/layout/search_instance.xml b/app/src/main/res/layout/search_instance.xml index 57272863e..ce2d5b894 100644 --- a/app/src/main/res/layout/search_instance.xml +++ b/app/src/main/res/layout/search_instance.xml @@ -31,5 +31,10 @@ android:layout_height="wrap_content" android:text="@string/pixelfed_instance" /> + \ No newline at end of file diff --git a/app/src/main/res/menu/remote_instances.xml b/app/src/main/res/menu/remote_instances.xml index 07ad2ed39..92e1ac2a7 100644 --- a/app/src/main/res/menu/remote_instances.xml +++ b/app/src/main/res/menu/remote_instances.xml @@ -37,6 +37,16 @@ + + + + + All these words (space-separated) None of these words (space-separated) Change column name + No Misskey instances + Misskey instance \ No newline at end of file