package im.actor.runtime.js.mvvm; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; import java.util.ArrayList; import java.util.List; import im.actor.runtime.Log; import im.actor.runtime.bser.BserObject; import im.actor.runtime.js.storage.JsListEngine; import im.actor.runtime.js.storage.JsListEngineCallback; import im.actor.runtime.js.utils.JsModernArray; import im.actor.runtime.storage.ListEngineItem; public class JsDisplayListBind<T extends JavaScriptObject, V extends BserObject & ListEngineItem> implements JsListEngineCallback<V> { /** * Underlying list engine */ private final JsListEngine<V> listEngine; /** * Underlying entity converted */ private final JsEntityConverter<V, T> entityConverter; /** * Convenience flag if overlays supported by entity converter */ private final boolean isOverlaysSupported; /** * Subscribers to bind */ private final JsDisplayListCallback<T> callback; /** * Is subscriber inverted */ private final boolean isInverted; /** * Current list values */ private ArrayList<V> values; /** * Current converted values */ private JsModernArray<T> jsValues; /** * Current overlay values (if supported) */ private JsModernArray<JavaScriptObject> jsOverlays; /** * Current dirty overlay items */ private ArrayList<Boolean> isOverlayDirty; /** * If all messages loaded from top of the list */ private boolean isOpenTop; /** * If all messages loaded from bottom of the list */ private boolean isOpenBottom; /** * Current window top limit */ private long windowTop; /** * Current window bottom limit */ private long windowBottom; /** * If list is inited and ready to receive list updates */ private boolean isInited; /** * If ForceReconvert required flag */ private boolean isForceReconverted = false; public JsDisplayListBind(JsDisplayListCallback<T> callback, boolean isInverted, JsListEngine<V> listEngine, JsEntityConverter<V, T> entityConverter) { this.callback = callback; this.isInverted = isInverted; this.listEngine = listEngine; this.entityConverter = entityConverter; this.isOverlaysSupported = entityConverter.isSupportOverlays(); this.values = new ArrayList<>(); this.jsValues = JsModernArray.createArray().cast(); if (isOverlaysSupported) { this.isOverlayDirty = new ArrayList<>(); this.jsOverlays = JsModernArray.createArray().cast(); } isInited = false; listEngine.addListener(this); } public ArrayList<V> getRawItems() { return values; } private void clearState() { values.clear(); jsValues.clear(); if (isOverlaysSupported) { jsOverlays.clear(); isOverlayDirty.clear(); } } public void initAll() { clearState(); long[] rids = listEngine.getOrderedIds(); for (long rid : rids) { V item = listEngine.getValue(rid); if (item == null) { Log.w("JsDisplayList", "Unable to find item #" + rid); continue; } values.add(item); jsValues.push(entityConverter.convert(item)); if (isOverlaysSupported) { jsOverlays.push(null); isOverlayDirty.add(true); } } processDirtyOverlays(); isInited = true; windowTop = 0; isOpenTop = true; windowBottom = 0; isOpenBottom = true; notifySubscriber(); } public void initTop(int limit) { clearState(); long[] rids = listEngine.getOrderedIds(); for (int i = 0; i < rids.length && i < limit; i++) { V item = listEngine.getValue(rids[i]); if (item == null) { Log.w("JsDisplayList", "Unable to find item #" + rids[i]); continue; } values.add(item); jsValues.push(entityConverter.convert(item)); if (isOverlaysSupported) { jsOverlays.push(null); isOverlayDirty.add(true); } } processDirtyOverlays(); isInited = true; windowTop = 0; isOpenTop = true; if (rids.length > 0) { windowBottom = rids[rids.length - 1]; isOpenBottom = false; } else { windowBottom = 0; isOpenBottom = true; } notifySubscriber(); } public void loadBottom(int limit) { if (!isInited) { return; } if (isOpenBottom) { return; } long[] rids = listEngine.getPrevIdsExclusive(windowBottom); for (int i = 0; i < rids.length && i < limit; i++) { V item = listEngine.getValue(rids[i]); if (item == null) { Log.w("JsDisplayList", "Unable to find item #" + rids[i]); continue; } values.add(item); jsValues.push(entityConverter.convert(item)); if (isOverlaysSupported) { jsOverlays.push(null); isOverlayDirty.add(true); } } processDirtyOverlays(); if (rids.length > 0) { windowBottom = rids[rids.length - 1]; isOpenBottom = false; } else { windowBottom = 0; isOpenBottom = true; } notifySubscriber(); } public void dispose() { listEngine.removeListener(this); } public void notifySubscriber() { if (isInverted) { if (isOverlaysSupported) { callback.onCollectionChanged(jsValues.reverse(), jsOverlays.reverse()); } else { callback.onCollectionChanged(jsValues.reverse(), null); } } else { if (isOverlaysSupported) { callback.onCollectionChanged(jsValues, jsOverlays); } else { callback.onCollectionChanged(jsValues, null); } } } private void addItemOrUpdateImpl(V item) { long id = item.getEngineId(); long sortKey = item.getEngineSort(); if (!isOpenTop && windowTop > sortKey) { // Doesn't fit in top window limit return; } if (!isOpenBottom && windowBottom < sortKey) { // Doesn't fit in bottom window limit return; } for (int i = 0; i < values.size(); i++) { if (values.get(i).getEngineId() == id) { values.remove(i); jsValues.remove(i); if (isOverlaysSupported) { markAsDirty(i); jsOverlays.remove(i); isOverlayDirty.remove(i); } break; } } for (int i = 0; i < values.size(); i++) { if (sortKey > values.get(i).getEngineSort()) { values.add(i, item); jsValues.insert(i, entityConverter.convert(item)); if (isOverlaysSupported) { jsOverlays.insert(i, null); isOverlayDirty.add(i, true); markAsDirty(i); } return; } } values.add(item); jsValues.push(entityConverter.convert(item)); if (isOverlaysSupported) { jsOverlays.push(null); isOverlayDirty.add(true); markAsDirty(values.size() - 1); } } private void remoteItemImpl(long id) { for (int i = 0; i < values.size(); i++) { if (values.get(i).getEngineId() == id) { values.remove(i); jsValues.remove(i); if (isOverlaysSupported) { markAsDirty(i); jsOverlays.remove(i); isOverlayDirty.add(true); } break; } } } /* * List Engine Updates */ @Override public void onItemAddedOrUpdated(V item) { if (!isInited) { return; } addItemOrUpdateImpl(item); processDirtyOverlays(); notifySubscriber(); } @Override public void onItemsAddedOrUpdated(List<V> items) { if (!isInited) { return; } for (V item : items) { addItemOrUpdateImpl(item); } processDirtyOverlays(); notifySubscriber(); } @Override public void onItemRemoved(long id) { if (!isInited) { return; } remoteItemImpl(id); processDirtyOverlays(); notifySubscriber(); } @Override public void onItemsRemoved(long[] ids) { if (!isInited) { return; } for (long id : ids) { remoteItemImpl(id); } processDirtyOverlays(); notifySubscriber(); } @Override public void onItemsReplaced(List<V> items) { if (!isInited) { return; } values.clear(); jsValues.clear(); if (isOverlaysSupported) { jsOverlays.clear(); isOverlayDirty.clear(); } onItemsAddedOrUpdated(items); } @Override public void onClear() { if (!isInited) { return; } values.clear(); jsValues.clear(); if (isOverlaysSupported) { jsOverlays.clear(); isOverlayDirty.clear(); } notifySubscriber(); } // // Reconverting // public void startReconverting() { isForceReconverted = false; } public void forceReconvert(long id) { for (int i = 0; i < values.size(); i++) { V value = values.get(i); if (value.getEngineId() == id) { jsValues.update(i, entityConverter.convert(value)); // Do not update overlays as this is method is a hack for binding isForceReconverted = true; break; } } } public void stopReconverting() { if (isForceReconverted) { isForceReconverted = false; notifySubscriber(); } } /* * Overlay support methods */ private boolean isDirty(int index) { return isOverlayDirty.get(index); } private void markAsDirty(int index) { isOverlayDirty.set(index, true); if (index - 1 >= 0) { isOverlayDirty.set(index - 1, true); } if (index + 1 < isOverlayDirty.size()) { isOverlayDirty.set(index + 1, true); } } private void markAsClean(int index) { isOverlayDirty.set(index, false); } private boolean processDirtyOverlays() { if (!isOverlaysSupported) { return false; } boolean isChanged = false; for (int i = 0; i < values.size(); i++) { if (!isDirty(i)) { continue; } V prev = null; V current = values.get(i); V next = null; if (i - 1 >= 0) { prev = values.get(i - 1); } if (i + 1 < values.size()) { next = values.get(i + 1); } jsOverlays.update(i, entityConverter.buildOverlay(prev, current, next)); markAsClean(i); isChanged = true; } return isChanged; } }