diff --git a/dist/bundle.es.js b/dist/bundle.es.js index 65b2192..989c7ac 100644 --- a/dist/bundle.es.js +++ b/dist/bundle.es.js @@ -778,6 +778,10 @@ const Lbry = { sync_hash: (params = {}) => daemonCallWithResult('sync_hash', params), sync_apply: (params = {}) => daemonCallWithResult('sync_apply', params), + // Preferences + preference_get: (params = {}) => daemonCallWithResult('preference_get', params), + preference_set: (params = {}) => daemonCallWithResult('preference_set', params), + // Comments comment_list: (params = {}) => daemonCallWithResult('comment_list', params), comment_create: (params = {}) => daemonCallWithResult('comment_create', params), @@ -3627,11 +3631,123 @@ const doToggleBlockChannel = uri => ({ } }); +/* eslint-disable */ +// underscore's deep equal function +// https://github.com/jashkenas/underscore/blob/master/underscore.js#L1189 + +function isEqual(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // `null` or `undefined` only equal to itself (strict comparison). + if (a == null || b == null) return false; + // `NaN`s are equivalent, but non-reflexive. + if (a !== a) return b !== b; + // Exhaust primitive checks + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + return deepEq(a, b, aStack, bStack); +} + +function deepEq(a, b, aStack, bStack) { + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + switch (className) { + // Strings, numbers, regular expressions, dates, and booleans are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN. + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + case '[object Symbol]': + return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); + } + + var areArrays = className === '[object Array]'; + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, + bCtor = b.constructor; + if (aCtor !== bCtor && !(typeof aCtor === 'function' && aCtor instanceof aCtor && typeof bCtor === 'function' && bCtor instanceof bCtor) && 'constructor' in a && 'constructor' in b) { + return false; + } + } + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + + // Initializing stack of traversed objects. + // It's done here since we only need them for objects and arrays comparison. + aStack = aStack || []; + bStack = bStack || []; + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + if (!isEqual(a[length], b[length], aStack, bStack)) return false; + } + } else { + // Deep compare objects. + var keys = Object.keys(a), + key; + length = keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (Object.keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = keys[length]; + if (!(has(b, key) && isEqual(a[key], b[key], aStack, bStack))) return false; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return true; +} + +function has(obj, path) { + return obj != null && hasOwnProperty.call(obj, path); +} +/* eslint-enable */ + var _extends$5 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +let oldShared; +const sharedPreferenceKey = 'shared'; + function extractUserState(rawObj) { - if (rawObj && rawObj.version === '0.1' && rawObj.shared) { - const { subscriptions, tags } = rawObj.shared; + if (rawObj && rawObj.version === '0.1' && rawObj.value) { + const { subscriptions, tags } = rawObj.value; return _extends$5({}, subscriptions ? { subscriptions } : {}, tags ? { tags } : {}); } @@ -3646,6 +3762,59 @@ function doPopulateSharedUserState(settings) { }; } +function sharedStateSubscriber(state, filters, version, accountId, walletId) { + // the shared object that will be saved + const shared = {}; + + Object.keys(filters).forEach(key => { + const filter = filters[key]; + const { source, property, transform } = filter; + let value = state[source][property]; + if (transform) { + value = transform(value); + } + + shared[key] = value; + }); + + if (!isEqual(oldShared, shared)) { + // only update if the preference changed from last call in the same session + oldShared = shared; + doPreferenceSet(sharedPreferenceKey, shared, version, accountId, walletId); + } +} + +function doPreferenceSet(key, value, version, accountId, walletId, success, fail) { + const preference = { + type: typeof value, + version, + value + }; + + lbryProxy.preference_set({ key, value: JSON.stringify(preference), account_id: accountId, wallet_id: walletId }).then(() => { + success(preference); + }).catch(() => { + if (fail) { + fail(); + } + }); +} + +function doPreferenceGet(key, accountId, walletId, success, fail) { + lbryProxy.preference_get({ key, account_id: accountId, wallet_id: walletId }).then(result => { + if (result) { + const preference = result[key]; + return success(preference); + } + + return success(null); + }).catch(err => { + if (fail) { + fail(err); + } + }); +} + var _extends$6 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; const reducers = {}; @@ -5048,6 +5217,8 @@ exports.doFocusSearchInput = doFocusSearchInput; exports.doGetNewAddress = doGetNewAddress; exports.doImportChannel = doImportChannel; exports.doPopulateSharedUserState = doPopulateSharedUserState; +exports.doPreferenceGet = doPreferenceGet; +exports.doPreferenceSet = doPreferenceSet; exports.doPrepareEdit = doPrepareEdit; exports.doPublish = doPublish; exports.doPurchaseUri = doPurchaseUri; @@ -5241,6 +5412,7 @@ exports.selectWalletUnlockPending = selectWalletUnlockPending; exports.selectWalletUnlockResult = selectWalletUnlockResult; exports.selectWalletUnlockSucceeded = selectWalletUnlockSucceeded; exports.setSearchApi = setSearchApi; +exports.sharedStateSubscriber = sharedStateSubscriber; exports.tagsReducer = tagsReducer; exports.toQueryString = toQueryString; exports.walletReducer = walletReducer; diff --git a/dist/flow-typed/Lbry.js b/dist/flow-typed/Lbry.js index 9bbde01..86691bc 100644 --- a/dist/flow-typed/Lbry.js +++ b/dist/flow-typed/Lbry.js @@ -202,6 +202,10 @@ declare type LbryTypes = { blob_delete: (params: {}) => Promise, blob_list: (params: {}) => Promise, + // Preferences + preference_get: (params: {}) => Promise, + preference_set: (params: {}) => Promise, + // Commenting comment_list: (params: {}) => Promise, comment_create: (params: {}) => Promise, diff --git a/flow-typed/Lbry.js b/flow-typed/Lbry.js index 9bbde01..86691bc 100644 --- a/flow-typed/Lbry.js +++ b/flow-typed/Lbry.js @@ -202,6 +202,10 @@ declare type LbryTypes = { blob_delete: (params: {}) => Promise, blob_list: (params: {}) => Promise, + // Preferences + preference_get: (params: {}) => Promise, + preference_set: (params: {}) => Promise, + // Commenting comment_list: (params: {}) => Promise, comment_create: (params: {}) => Promise, diff --git a/src/index.js b/src/index.js index ced22a0..650da90 100644 --- a/src/index.js +++ b/src/index.js @@ -114,7 +114,12 @@ export { doCommentList, doCommentCreate } from 'redux/actions/comments'; export { doToggleBlockChannel } from 'redux/actions/blocked'; -export { doPopulateSharedUserState } from 'redux/actions/sync'; +export { + doPopulateSharedUserState, + doPreferenceGet, + doPreferenceSet, + sharedStateSubscriber +} from 'redux/actions/sync'; // utils export { batchActions } from 'util/batch-actions'; diff --git a/src/lbry.js b/src/lbry.js index dfdbbf2..36db557 100644 --- a/src/lbry.js +++ b/src/lbry.js @@ -110,6 +110,10 @@ const Lbry: LbryTypes = { sync_hash: (params = {}) => daemonCallWithResult('sync_hash', params), sync_apply: (params = {}) => daemonCallWithResult('sync_apply', params), + // Preferences + preference_get: (params = {}) => daemonCallWithResult('preference_get', params), + preference_set: (params = {}) => daemonCallWithResult('preference_set', params), + // Comments comment_list: (params = {}) => daemonCallWithResult('comment_list', params), comment_create: (params = {}) => daemonCallWithResult('comment_create', params), diff --git a/src/redux/actions/sync.js b/src/redux/actions/sync.js index ffcf2e7..0bde0d6 100644 --- a/src/redux/actions/sync.js +++ b/src/redux/actions/sync.js @@ -1,17 +1,22 @@ // @flow import * as ACTIONS from 'constants/action_types'; +import Lbry from 'lbry'; +import isEqual from 'util/deep-equal'; -type v0Data = { +type sharedData = { version: '0.1', - shared: { + value: { subscriptions?: Array, tags?: Array, }, }; -function extractUserState(rawObj: v0Data) { - if (rawObj && rawObj.version === '0.1' && rawObj.shared) { - const { subscriptions, tags } = rawObj.shared; +let oldShared; +const sharedPreferenceKey = 'shared'; + +function extractUserState(rawObj: sharedData) { + if (rawObj && rawObj.version === '0.1' && rawObj.value) { + const { subscriptions, tags } = rawObj.value; return { ...(subscriptions ? { subscriptions } : {}), @@ -28,3 +33,73 @@ export function doPopulateSharedUserState(settings: any) { dispatch({ type: ACTIONS.USER_STATE_POPULATE, data: { subscriptions, tags } }); }; } + +export function sharedStateSubscriber( + state: any, + filters: any, + version: string, + accountId: string, + walletId: string) +{ + // the shared object that will be saved + const shared = {}; + + Object.keys(filters).forEach(key => { + const filter = filters[key]; + const { source, property, transform } = filter; + let value = state[source][property]; + if (transform) { + value = transform(value); + } + + shared[key] = value; + }); + + + if (!isEqual(oldShared, shared)) { + // only update if the preference changed from last call in the same session + oldShared = shared; + doPreferenceSet(sharedPreferenceKey, shared, version, accountId, walletId); + } +} + +export function doPreferenceSet( + key: string, + value: any, + version: string, + accountId: string, + walletId: string, + success: Function, + fail: Function +) { + const preference = { + type: (typeof value), + version, + value, + }; + + Lbry.preference_set({ key, value: JSON.stringify(preference), account_id: accountId, wallet_id: walletId }).then(() => { + success(preference); + }).catch(() => { + if (fail) { fail(); } + }); +} + +export function doPreferenceGet( + key: string, + accountId: string, + walletId: string, + success: Function, + fail: Function +) { + Lbry.preference_get({ key, account_id: accountId, wallet_id: walletId }).then(result => { + if (result) { + const preference = result[key]; + return success(preference); + } + + return success(null); + }).catch(err => { + if (fail) { fail(err); } + }); +} diff --git a/src/util/deep-equal.js b/src/util/deep-equal.js new file mode 100644 index 0000000..1925763 --- /dev/null +++ b/src/util/deep-equal.js @@ -0,0 +1,117 @@ +/* eslint-disable */ +// underscore's deep equal function +// https://github.com/jashkenas/underscore/blob/master/underscore.js#L1189 + +export default function isEqual(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // `null` or `undefined` only equal to itself (strict comparison). + if (a == null || b == null) return false; + // `NaN`s are equivalent, but non-reflexive. + if (a !== a) return b !== b; + // Exhaust primitive checks + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + return deepEq(a, b, aStack, bStack); +} + +function deepEq(a, b, aStack, bStack) { + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + switch (className) { + // Strings, numbers, regular expressions, dates, and booleans are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN. + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + case '[object Symbol]': + return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); + } + + var areArrays = className === '[object Array]'; + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, + bCtor = b.constructor; + if ( + aCtor !== bCtor && + !( + typeof aCtor === 'function' && + aCtor instanceof aCtor && + typeof bCtor === 'function' && + bCtor instanceof bCtor + ) && + ('constructor' in a && 'constructor' in b) + ) { + return false; + } + } + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + + // Initializing stack of traversed objects. + // It's done here since we only need them for objects and arrays comparison. + aStack = aStack || []; + bStack = bStack || []; + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + if (!isEqual(a[length], b[length], aStack, bStack)) return false; + } + } else { + // Deep compare objects. + var keys = Object.keys(a), + key; + length = keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (Object.keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = keys[length]; + if (!(has(b, key) && isEqual(a[key], b[key], aStack, bStack))) return false; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return true; +} + +function has(obj, path) { + return obj != null && hasOwnProperty.call(obj, path); +} +/* eslint-enable */