diff --git a/Habitica/AndroidManifest.xml b/Habitica/AndroidManifest.xml index ef7893c8c..3e6f3bc72 100644 --- a/Habitica/AndroidManifest.xml +++ b/Habitica/AndroidManifest.xml @@ -2,7 +2,7 @@ diff --git a/Habitica/res/layout/daily_item_card.xml b/Habitica/res/layout/daily_item_card.xml index 3071d4388..3f7ab165e 100644 --- a/Habitica/res/layout/daily_item_card.xml +++ b/Habitica/res/layout/daily_item_card.xml @@ -147,6 +147,23 @@ + + + + + + + + fun retrieveCompletedTodos(userId: String): Flowable + fun syncErroredTasks(): Single> } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImpl.kt index 5055bbf01..113707da2 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImpl.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImpl.kt @@ -9,8 +9,10 @@ import com.habitrpg.android.habitica.models.responses.TaskDirection import com.habitrpg.android.habitica.models.responses.TaskScoringResult import com.habitrpg.android.habitica.models.tasks.* import com.habitrpg.android.habitica.models.user.User +import com.playseeds.android.sdk.inappmessaging.Log import io.reactivex.Flowable import io.reactivex.Maybe +import io.reactivex.Single import io.reactivex.functions.Consumer import io.realm.Realm import io.realm.RealmList @@ -162,12 +164,26 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli } } + task.isSaving = true + task.isCreating = true + task.hasErrored = false + task.userId = userID + if (task.id == null) { + task.id = UUID.randomUUID().toString() + } + localRepository.saveTask(task) + return apiClient.createTask(task) .map { task1 -> task1.dateCreated = Date() task1 } .doOnNext { localRepository.saveTask(it) } + .doOnError { + task.hasErrored = true + task.isSaving = false + localRepository.save(task) + } } @Suppress("ReturnCount") @@ -178,6 +194,10 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli } lastTaskAction = now val id = task.id ?: return Maybe.just(task) + + task.isSaving = true + task.hasErrored = false + localRepository.saveTask(task) return localRepository.getTaskCopy(id).firstElement() .flatMap { task1 -> apiClient.updateTask(id, task1).singleElement() } .map { task1 -> @@ -185,6 +205,11 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli task1 } .doOnSuccess { localRepository.saveTask(it) } + .doOnError { + task.hasErrored = true + task.isSaving = false + localRepository.save(task) + } } override fun deleteTask(taskId: String): Flowable { @@ -241,4 +266,17 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli return apiClient.getTasks("dailys", formatter.format(date)) .flatMapMaybe { localRepository.updateIsdue(it) } } + + override fun syncErroredTasks(): Single> { + return localRepository.getErroredTasks(userID).firstElement() + .flatMapPublisher { Flowable.fromIterable(it) } + .map { localRepository.getUnmanagedCopy(it) } + .flatMap { + return@flatMap if (it.isCreating) { + createTask(it) + } else { + updateTask(it).toFlowable() + } + }.toList() + } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/TaskLocalRepository.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/TaskLocalRepository.kt index cb2267a8b..a6af4dfae 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/TaskLocalRepository.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/TaskLocalRepository.kt @@ -34,4 +34,5 @@ interface TaskLocalRepository : BaseLocalRepository { fun updateTaskPositions(taskOrder: List) fun saveCompletedTodos(userId: String, tasks: MutableCollection) + fun getErroredTasks(userID: String): Flowable> } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmTaskLocalRepository.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmTaskLocalRepository.kt index 0cae3910d..d72cb29b9 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmTaskLocalRepository.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmTaskLocalRepository.kt @@ -203,4 +203,14 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), } } } + + override fun getErroredTasks(userID: String): Flowable> { + return realm.where(Task::class.java) + .equalTo("userId", userID) + .equalTo("hasErrored", true) + .sort("position") + .findAll() + .asFlowable() + .filter { it.isLoaded } + .retry(1) } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt index aaa1e0460..6bca1b823 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt @@ -18,6 +18,9 @@ import org.json.JSONException import java.util.* open class Task : RealmObject, Parcelable { + @PrimaryKey + @SerializedName("_id") + var id: String? = null var userId: String = "" var priority: Float = 0.0f var text: String = "" @@ -60,9 +63,6 @@ open class Task : RealmObject, Parcelable { var parsedText: CharSequence? = null @Ignore var parsedNotes: CharSequence? = null - @PrimaryKey - @SerializedName("_id") - var id: String? = null set(value) { field = value repeat?.taskId = id @@ -73,6 +73,11 @@ open class Task : RealmObject, Parcelable { var nextDue: Date? = null var yesterDaily: Boolean? = null + //Needed for offline creating/updating + var isSaving: Boolean = false + var hasErrored: Boolean = false + var isCreating: Boolean = false + private var daysOfMonthString: String? = null private var weeksOfMonthString: String? = null diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RealmBaseTasksRecyclerViewAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RealmBaseTasksRecyclerViewAdapter.kt index 8fffcbf4b..c0942be47 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RealmBaseTasksRecyclerViewAdapter.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RealmBaseTasksRecyclerViewAdapter.kt @@ -7,6 +7,11 @@ import android.view.ViewGroup import com.habitrpg.android.habitica.helpers.TaskFilterHelper import com.habitrpg.android.habitica.models.tasks.Task import com.habitrpg.android.habitica.ui.viewHolders.tasks.BaseTaskViewHolder +import io.reactivex.BackpressureStrategy +import io.reactivex.Flowable +import io.reactivex.Observable +import io.reactivex.functions.Action +import io.reactivex.subjects.PublishSubject import io.realm.OrderedRealmCollection import io.realm.OrderedRealmCollectionChangeListener import io.realm.RealmList @@ -45,6 +50,8 @@ abstract class RealmBaseTasksRecyclerViewAdapter(privat var data: OrderedRealmCollection? = null private set + private var errorButtonEventsSubject = PublishSubject.create() + private val isDataValid: Boolean get() = data?.isValid ?: false @@ -78,8 +85,6 @@ abstract class RealmBaseTasksRecyclerViewAdapter(privat fun getItem(index: Int): Task? = if (isDataValid) data?.get(index) else null - - override fun updateData(data: OrderedRealmCollection?) { if (hasAutoUpdates) { if (isDataValid) { @@ -132,6 +137,9 @@ abstract class RealmBaseTasksRecyclerViewAdapter(privat val item = getItem(position) if (item != null) { holder.bindHolder(item, position) + holder.errorButtonClicked = Action { + errorButtonEventsSubject.onNext("") + } } } @@ -158,4 +166,8 @@ abstract class RealmBaseTasksRecyclerViewAdapter(privat override fun getTaskIDAt(position: Int): String { return data?.get(position)?.id ?: "" } + + override fun getErrorButtonEvents(): Flowable { + return errorButtonEventsSubject.toFlowable(BackpressureStrategy.DROP) + } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RewardsRecyclerViewAdapter.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RewardsRecyclerViewAdapter.java index 72c349b29..588a4fa77 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RewardsRecyclerViewAdapter.java +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RewardsRecyclerViewAdapter.java @@ -15,6 +15,10 @@ import com.habitrpg.android.habitica.models.user.User; import com.habitrpg.android.habitica.ui.viewHolders.ShopItemViewHolder; import com.habitrpg.android.habitica.ui.viewHolders.tasks.RewardViewHolder; +import io.reactivex.BackpressureStrategy; +import io.reactivex.Flowable; +import io.reactivex.functions.Action; +import io.reactivex.subjects.PublishSubject; import io.realm.OrderedRealmCollection; public class RewardsRecyclerViewAdapter extends RecyclerView.Adapter implements TaskRecyclerViewAdapter { @@ -30,6 +34,8 @@ public class RewardsRecyclerViewAdapter extends RecyclerView.Adapter errorButtonEvents = PublishSubject.create(); + public RewardsRecyclerViewAdapter(@Nullable OrderedRealmCollection data, Context context, int layoutResource, @Nullable User user) { this.context = context; this.layoutResource = layoutResource; @@ -91,6 +97,11 @@ public class RewardsRecyclerViewAdapter extends RecyclerView.Adapter getErrorButtonEvents() { + return errorButtonEvents.toFlowable(BackpressureStrategy.DROP); + } + @Override public int getItemCount() { int rewardCount = getCustomRewardCount(); diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/TaskRecyclerViewAdapter.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/TaskRecyclerViewAdapter.java index c3287d060..a5ca0c729 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/TaskRecyclerViewAdapter.java +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/TaskRecyclerViewAdapter.java @@ -4,6 +4,8 @@ import com.habitrpg.android.habitica.models.tasks.Task; import org.jetbrains.annotations.Nullable; +import io.reactivex.Flowable; +import io.reactivex.Observable; import io.realm.OrderedRealmCollection; import io.realm.RealmResults; @@ -22,4 +24,6 @@ public interface TaskRecyclerViewAdapter { boolean getIgnoreUpdates(); void updateUnfilteredData(@Nullable OrderedRealmCollection data); + + Flowable getErrorButtonEvents(); } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TaskRecyclerViewFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TaskRecyclerViewFragment.kt index 848f86ae2..35419d5dc 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TaskRecyclerViewFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TaskRecyclerViewFragment.kt @@ -83,9 +83,13 @@ open class TaskRecyclerViewFragment : BaseFragment(), View.OnClickListener, Swip allowReordering() } - recyclerAdapter = adapter as TaskRecyclerViewAdapter + recyclerAdapter = adapter as? TaskRecyclerViewAdapter recyclerView.adapter = adapter + recyclerAdapter?.errorButtonEvents?.subscribe(Consumer { + taskRepository.syncErroredTasks().subscribe(Consumer {}, RxErrorHandler.handleEmptyError()) + }, RxErrorHandler.handleEmptyError()) + if (this.classType != null) { taskRepository.getTasks(this.classType ?: "", userID).firstElement().subscribe(Consumer { this.recyclerAdapter?.updateUnfilteredData(it) this.recyclerAdapter?.filter() diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/BaseTaskViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/BaseTaskViewHolder.kt index b0be1aac7..265ae9924 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/BaseTaskViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/BaseTaskViewHolder.kt @@ -4,10 +4,7 @@ import android.content.Context import android.support.v7.widget.RecyclerView import android.text.method.LinkMovementMethod import android.view.View -import android.widget.Button -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView +import android.widget.* import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.events.TaskTappedEvent import com.habitrpg.android.habitica.ui.helpers.bindView @@ -20,6 +17,7 @@ import com.habitrpg.android.habitica.ui.helpers.bindOptionalView import com.habitrpg.android.habitica.ui.views.EllipsisTextView import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.Action import io.reactivex.functions.Consumer import io.reactivex.schedulers.Schedulers import net.pherth.android.emoji_library.EmojiTextView @@ -29,6 +27,7 @@ abstract class BaseTaskViewHolder constructor(itemView: View) : RecyclerView.Vie var task: Task? = null + var errorButtonClicked: Action? = null protected var context: Context private val titleTextView: EllipsisTextView by bindView(itemView, R.id.checkedTextView) private val notesTextView: EllipsisTextView by bindView(itemView, R.id.notesTextView) @@ -40,6 +39,8 @@ abstract class BaseTaskViewHolder constructor(itemView: View) : RecyclerView.Vie private val taskIconWrapper: LinearLayout? by bindView(itemView, R.id.taskIconWrapper) private val approvalRequiredTextView: TextView? by bindView(itemView, R.id.approvalRequiredTextField) private val expandNotesButton: Button by bindView(R.id.expand_notes_button) + private val syncingView: ProgressBar by bindView(R.id.syncing_view) + private val errorIconView: ImageButton by bindView(R.id.error_icon) protected val taskGray: Int by bindColor(itemView.context, R.color.task_gray) private var openTaskDisabled: Boolean = false @@ -73,6 +74,8 @@ abstract class BaseTaskViewHolder constructor(itemView: View) : RecyclerView.Vie itemView.setOnClickListener { onClick(it) } itemView.isClickable = true + errorIconView.setOnClickListener { errorButtonClicked?.run()} + //Re enable when we find a way to only react when a link is tapped. //notesTextView.movementMethod = LinkMovementMethod.getInstance() //titleTextView.movementMethod = LinkMovementMethod.getInstance() @@ -163,6 +166,8 @@ abstract class BaseTaskViewHolder constructor(itemView: View) : RecyclerView.Vie approvalRequiredTextView?.visibility = View.GONE } + syncingView.visibility = if (task?.isSaving == true) View.VISIBLE else View.GONE + errorIconView.visibility = if (task?.hasErrored == true) View.VISIBLE else View.GONE }