Tooting of text toots

This commit is contained in:
Grishka 2022-01-17 21:50:48 +03:00
parent dfbc1fd2e2
commit b3a99e0764
13 changed files with 260 additions and 11 deletions

View File

@ -32,8 +32,10 @@ dependencies {
implementation 'me.grishka.litex:recyclerview:1.2.1' implementation 'me.grishka.litex:recyclerview:1.2.1'
implementation 'me.grishka.litex:swiperefreshlayout:1.1.0' implementation 'me.grishka.litex:swiperefreshlayout:1.1.0'
implementation 'me.grishka.litex:browser:1.4.0' implementation 'me.grishka.litex:browser:1.4.0'
implementation 'me.grishka.appkit:appkit:1.1' implementation 'me.grishka.appkit:appkit:1.1.1'
implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3' implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'
implementation 'de.psdev:async-otto:1.0.3'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
} }

View File

@ -12,7 +12,7 @@
android:theme="@style/Theme.Mastodon" android:theme="@style/Theme.Mastodon"
android:largeHeap="true"> android:largeHeap="true">
<activity android:name=".MainActivity" android:exported="true" android:configChanges="orientation|screenSize"> <activity android:name=".MainActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>

View File

@ -0,0 +1,22 @@
package org.joinmastodon.android;
import com.squareup.otto.AsyncBus;
/**
* Created by grishka on 24.08.15.
*/
public class E{
private static AsyncBus bus=new AsyncBus();
public static void post(Object event){
bus.post(event);
}
public static void register(Object listener){
bus.register(listener);
}
public static void unregister(Object listener){
bus.unregister(listener);
}
}

View File

@ -25,6 +25,7 @@ import java.io.Reader;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Map;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -76,6 +77,12 @@ public class MastodonAPIController{
if(token!=null) if(token!=null)
builder.header("Authorization", "Bearer "+token); builder.header("Authorization", "Bearer "+token);
if(req.headers!=null){
for(Map.Entry<String, String> header:req.headers.entrySet()){
builder.header(header.getKey(), header.getValue());
}
}
Request hreq=builder.build(); Request hreq=builder.build();
Call call=httpClient.newCall(hreq); Call call=httpClient.newCall(hreq);
synchronized(req){ synchronized(req){

View File

@ -33,6 +33,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
Call okhttpCall; Call okhttpCall;
Token token; Token token;
boolean canceled; boolean canceled;
Map<String, String> headers;
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){ public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
this.path=path; this.path=path;
@ -89,6 +90,12 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
queryParams.put(key, value); queryParams.put(key, value);
} }
protected void addHeader(String key, String value){
if(headers==null)
headers=new HashMap<>();
headers.put(key, value);
}
protected String getPathPrefix(){ protected String getPathPrefix(){
return "/api/v1"; return "/api/v1";
} }

View File

@ -0,0 +1,36 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
public class CreateStatus extends MastodonAPIRequest<Status>{
public CreateStatus(CreateStatus.Request req, String uuid){
super(HttpMethod.POST, "/statuses", Status.class);
setRequestBody(req);
addHeader("Idempotency-Key", uuid);
}
public static class Request{
public String status;
public List<String> mediaIds;
public Poll poll;
public String inReplyToId;
public boolean sensitive;
public String spoilerText;
public StatusPrivacy visibility;
public Instant scheduledAt;
public String language;
public static class Poll{
public ArrayList<String> options=new ArrayList<>();
public int expiresIn;
public boolean multiple;
public boolean hideTotals;
}
}
}

View File

@ -0,0 +1,11 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.Status;
public class StatusCreatedEvent{
public Status status;
public StatusCreatedEvent(Status status){
this.status=status;
}
}

View File

@ -0,0 +1,88 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Status;
import java.util.UUID;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
public class CreateTootFragment extends ToolbarFragment{
private EditText mainEditText;
private String accountID;
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setHasOptionsMenu(true);
accountID=getArguments().getString("account");
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_new_toot, container, false);
mainEditText=view.findViewById(R.id.toot_text);
return view;
}
@Override
public void onResume(){
super.onResume();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
view.postDelayed(()->{
mainEditText.requestFocus();
imm.showSoftInput(mainEditText, 0);
}, 100);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
menu.add("TOOT!").setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
String text=mainEditText.getText().toString();
CreateStatus.Request req=new CreateStatus.Request();
req.status=text;
String uuid=UUID.randomUUID().toString();
new CreateStatus(req, uuid)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
Nav.finish(CreateTootFragment.this);
E.post(new StatusCreatedEvent(result));
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.exec(accountID);
return true;
}
}

View File

@ -1,13 +1,23 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import java.util.Collections;
import java.util.List; import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
public class HomeTimelineFragment extends StatusListFragment{ public class HomeTimelineFragment extends StatusListFragment{
@ -17,6 +27,7 @@ public class HomeTimelineFragment extends StatusListFragment{
public void onAttach(Activity activity){ public void onAttach(Activity activity){
super.onAttach(activity); super.onAttach(activity);
setTitle(R.string.app_name); setTitle(R.string.app_name);
setHasOptionsMenu(true);
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
loadData(); loadData();
} }
@ -32,4 +43,34 @@ public class HomeTimelineFragment extends StatusListFragment{
}) })
.exec(accountID); .exec(accountID);
} }
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
menu.add("New toot");
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), CreateTootFragment.class, args);
return true;
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Subscribe
public void onStatusCreated(StatusCreatedEvent ev){
prependItems(Collections.singletonList(ev.status));
}
} }

View File

@ -18,6 +18,7 @@ import me.grishka.appkit.views.UsableRecyclerView;
public abstract class StatusListFragment extends BaseRecyclerFragment<Status>{ public abstract class StatusListFragment extends BaseRecyclerFragment<Status>{
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>(); protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
private DisplayItemsAdapter adapter;
public StatusListFragment(){ public StatusListFragment(){
super(20); super(20);
@ -25,7 +26,7 @@ public abstract class StatusListFragment extends BaseRecyclerFragment<Status>{
@Override @Override
protected RecyclerView.Adapter getAdapter(){ protected RecyclerView.Adapter getAdapter(){
return new DisplayItemsAdapter(); return adapter=new DisplayItemsAdapter();
} }
@Override @Override
@ -42,6 +43,17 @@ public abstract class StatusListFragment extends BaseRecyclerFragment<Status>{
displayItems.clear(); displayItems.clear();
} }
protected void prependItems(List<Status> items){
data.addAll(0, items);
int offset=0;
for(Status s:items){
List<StatusDisplayItem> toAdd=StatusDisplayItem.buildItems(this, s);
displayItems.addAll(offset, toAdd);
offset+=toAdd.size();
}
adapter.notifyItemRangeInserted(0, offset);
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{ protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){ public DisplayItemsAdapter(){

View File

@ -2,6 +2,7 @@ package org.joinmastodon.android.model;
import android.text.Html; import android.text.Html;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField; import org.joinmastodon.android.api.RequiredField;
@ -54,8 +55,6 @@ public class Status extends BaseModel{
public boolean bookmarked; public boolean bookmarked;
public boolean pinned; public boolean pinned;
public transient CharSequence processedContent;
@Override @Override
public void postprocess() throws ObjectValidationException{ public void postprocess() throws ObjectValidationException{
super.postprocess(); super.postprocess();
@ -76,10 +75,6 @@ public class Status extends BaseModel{
card.postprocess(); card.postprocess();
if(reblog!=null) if(reblog!=null)
reblog.postprocess(); reblog.postprocess();
if(!TextUtils.isEmpty(content)){
processedContent=HtmlParser.parse(content, emojis);
}
} }
@Override @Override
@ -114,7 +109,6 @@ public class Status extends BaseModel{
", muted="+muted+ ", muted="+muted+
", bookmarked="+bookmarked+ ", bookmarked="+bookmarked+
", pinned="+pinned+ ", pinned="+pinned+
", processedContent="+processedContent+
'}'; '}';
} }
} }

View File

@ -7,6 +7,7 @@ import android.view.ViewGroup;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.text.HtmlParser;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -49,7 +50,7 @@ public abstract class StatusDisplayItem{
} }
items.add(new HeaderStatusDisplayItem(status, statusForContent.account, statusForContent.createdAt)); items.add(new HeaderStatusDisplayItem(status, statusForContent.account, statusForContent.createdAt));
if(!TextUtils.isEmpty(statusForContent.content)) if(!TextUtils.isEmpty(statusForContent.content))
items.add(new TextStatusDisplayItem(status, statusForContent.processedContent, fragment)); items.add(new TextStatusDisplayItem(status, HtmlParser.parse(statusForContent.content, statusForContent.emojis), fragment));
for(Attachment attachment:statusForContent.mediaAttachments){ for(Attachment attachment:statusForContent.mediaAttachments){
if(attachment.type==Attachment.Type.IMAGE){ if(attachment.type==Attachment.Type.IMAGE){
items.add(new PhotoStatusDisplayItem(status, attachment)); items.add(new PhotoStatusDisplayItem(status, attachment));

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/toot_text"
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1"
android:gravity="top"
android:inputType="textMultiLine|textCapSentences"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<TextView
android:id="@+id/char_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>