-
Notifications
You must be signed in to change notification settings - Fork 18
/
ChatFragment.java
223 lines (187 loc) · 9.11 KB
/
ChatFragment.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package org.hit.android.haim.texasholdem.view.fragment.chat;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.view.animation.OvershootInterpolator;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import org.hit.android.haim.texasholdem.R;
import org.hit.android.haim.texasholdem.common.model.bean.chat.Channel;
import org.hit.android.haim.texasholdem.common.model.bean.chat.Message;
import org.hit.android.haim.texasholdem.databinding.FragmentChatBinding;
import org.hit.android.haim.texasholdem.model.chat.Chat;
import org.hit.android.haim.texasholdem.model.game.Game;
import org.hit.android.haim.texasholdem.view.activity.MainActivity;
import org.hit.android.haim.texasholdem.view.fragment.ViewBindedFragment;
import java.util.ArrayList;
import java.util.List;
import jp.wasabeef.recyclerview.adapters.AlphaInAnimationAdapter;
import jp.wasabeef.recyclerview.adapters.ScaleInAnimationAdapter;
import jp.wasabeef.recyclerview.animators.FlipInBottomXAnimator;
/**
* The chat fragment responsible for showing messages arrived from other players, and
* to let the current player to send messages in a chat.<br/>
* It is available for use during online games only.
* @author Haim Adrian
* @since 14-Apr-21
*/
public class ChatFragment extends ViewBindedFragment<FragmentChatBinding> implements Chat.ChatListener {
private static final String LOGGER = ChatFragment.class.getSimpleName();
private static final String STORED_CHANNEL_NAME_KEY = ChatFragment.class.getName() + ".CHANNEL_NAME";
/**
* Game hash is used as channel name
*/
private String channelName;
/**
* {@link MessageCardAdapter} is used for displaying messages in the chat (in the recycler view)
*/
private MessageCardAdapter messageCardAdapter;
/**
* Keep a reference to {@link MainActivity}, so we can handle HTTP failures at one location.
*/
private MainActivity mainActivity;
/**
* Keep a reference to this fragment's view, so we can use it for toasts
*/
private View fragmentView;
/**
* Hold a handler as a data member, cause it might be garbage collected before executing tasks that we submit.<br/>
* When handler refers to null, it means the view is destroyed, and a post action should be discarded.
*/
private Handler handler;
/**
* Constructs a new {@link ChatFragment}, using the game hash as channel name, so we can
* work with backend on the messages in a game's chat.
*/
public ChatFragment() {
super(R.layout.fragment_chat, FragmentChatBinding::bind);
Log.d(LOGGER, this.toString() + ".new");
this.channelName = Game.getInstance().getGameHash();
}
/**
* Constructs a new {@link ChatFragment}, using the game hash as channel name, so we can
* work with backend on the messages in a game's chat.
* @param channelName Game hash is used as channel name
*/
public ChatFragment(String channelName) {
super(R.layout.fragment_chat, FragmentChatBinding::bind);
Log.d(LOGGER, this.toString() + ".new");
this.channelName = channelName;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
handler = new Handler(Looper.myLooper());
if ((savedInstanceState != null) && savedInstanceState.containsKey(STORED_CHANNEL_NAME_KEY)) {
channelName = savedInstanceState.getString(STORED_CHANNEL_NAME_KEY);
}
fragmentView = view;
mainActivity = (MainActivity) getActivity();
if (channelName != null) {
getBinding().textViewChatName.setText(channelName);
}
getBinding().imageViewSend.setOnClickListener(this::onSendButtonClicked);
getBinding().imageViewBack.setOnClickListener(v -> ((MainActivity)getActivity()).navigateBack());
getBinding().messagesRecyclerView.setHasFixedSize(true);
// Set vertical layout and custom animator to the recycler view
LinearLayoutManager layoutManager = new LinearLayoutManager(mainActivity, LinearLayoutManager.VERTICAL, false);
//layoutManager.setStackFromEnd(true);
getBinding().messagesRecyclerView.setLayoutManager(layoutManager);
getBinding().messagesRecyclerView.setItemAnimator(new FlipInBottomXAnimator(new OvershootInterpolator()));
// When we start, make sure we get all messages in channel to display old messages
List<Message> data = new ArrayList<>(Game.getInstance().getChat().getMessages());
initializeMessagesCardAdapter(data, getBinding().messagesRecyclerView);
// Register chat listener, so we will receive updates on new messages
Game.getInstance().getChat().addChatListener(this);
// Update the participants text view with details from server
updateChatParticipantsTextView();
}
@Override
public void onDestroyView() {
// Remove the listener so we will not register ourselves over and over
// in case onCreateView is raised more than once.
Game.getInstance().getChat().removeChatListener(this);
fragmentView = null;
messageCardAdapter = null;
handler = null;
super.onDestroyView();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(STORED_CHANNEL_NAME_KEY, channelName);
}
@Override
public void onMessageArrived(Message message) {
if (handler != null) {
handler.post(() -> {
if (handler != null) {
int messagePosition = messageCardAdapter.getItemCount();
messageCardAdapter.getMessages().add(message);
messageCardAdapter.notifyItemInserted(messagePosition);
messageCardAdapter.notifyItemRangeChanged(messagePosition, messageCardAdapter.getMessages().size());
getBinding().messagesRecyclerView.scrollToPosition(messagePosition);
updateChatParticipantsTextView();
}
});
}
}
@Override
public void onChatError(String errorMessage) {
if (handler != null) {
handler.post(() -> {
if (handler != null) {
Snackbar.make(fragmentView, errorMessage, Snackbar.LENGTH_LONG).show();
}
});
}
}
/**
* Update the text view with amount of participants in chat
*/
private void updateChatParticipantsTextView() {
Channel channelInfo = Game.getInstance().getChat().getChannelInfo();
if (channelInfo != null) {
getBinding().textViewChatParticipants.setText(String.format(getString(R.string.participants), channelInfo.getUsers().size()));
}
}
/**
* Create a {@link MessageCardAdapter} to set to a specified recycler view.<br/>
* When there is data, we will scroll the recycler view to the latest message, in order to display
* the most recent messages
* @param data Existing messages to display as cards in the specified recycler view
* @param messagesRecyclerView The messages recycler view
*/
private void initializeMessagesCardAdapter(List<Message> data, RecyclerView messagesRecyclerView) {
messageCardAdapter = new MessageCardAdapter(data, mainActivity.getUser());
messagesRecyclerView.setAdapter(new ScaleInAnimationAdapter(new AlphaInAnimationAdapter(messageCardAdapter)));
if (!data.isEmpty()) {
messagesRecyclerView.scrollToPosition(data.size() - 1);
}
}
/**
* Occurs when user presses the "Send" button.<br/>
* We will get the input message that user entered and send it to the server so other players will see it.<br/>
* In case the input message is empty, we show a toast to the user, telling him to write something
* @param view Sender
*/
void onSendButtonClicked(View view) {
if ((messageCardAdapter != null) && (channelName != null)) {
String message = getBinding().editTextWriteMessage.getText().toString().trim();
getBinding().editTextWriteMessage.setText("");
if (message.isEmpty()) {
Toast.makeText(mainActivity, "Cannot send empty message", Toast.LENGTH_SHORT).show();
} else {
Game.getInstance().getChat().sendMessage(mainActivity.getUser().getId(), message, error -> Snackbar.make(fragmentView, error, Snackbar.LENGTH_LONG).show());
}
} else {
Toast.makeText(mainActivity, "Cannot create messages. Reload.", Toast.LENGTH_LONG).show();
}
}
}