diff --git a/build/ci/production-values.yaml b/build/ci/production-values.yaml index 6652bfd1d4..0d63e9ec79 100644 --- a/build/ci/production-values.yaml +++ b/build/ci/production-values.yaml @@ -86,6 +86,7 @@ nodejs: limits: memory: "1500Mi" cpu: "1500m" + heapSize: 1250 varnish: logging: {} replicaCount: 1 diff --git a/helm-chart/sefaria/templates/rollout/nodejs.yaml b/helm-chart/sefaria/templates/rollout/nodejs.yaml index 5fc4b89a6c..47d544282d 100644 --- a/helm-chart/sefaria/templates/rollout/nodejs.yaml +++ b/helm-chart/sefaria/templates/rollout/nodejs.yaml @@ -62,6 +62,8 @@ spec: value: redis-{{ .Values.deployEnv }} - name: DEBUG value: {{ .Values.localSettings.DEBUG | quote }} + - name: NODE_OPTIONS + value: "--max-old-space-size={{ .Values.nodejs.heapSize }}" envFrom: - configMapRef: name: local-settings-node-{{ .Values.deployEnv }} diff --git a/helm-chart/sefaria/values.yaml b/helm-chart/sefaria/values.yaml index cda1b5ac1c..36ed541fca 100644 --- a/helm-chart/sefaria/values.yaml +++ b/helm-chart/sefaria/values.yaml @@ -12,7 +12,7 @@ sandbox: "false" # suggestions for the values are prod/dev/test deployEnv: "dev" -# Helps create services for nginx, nodeja, varnish and web pods with appropriate +# Helps create services for nginx, nodejs, varnish and web pods with appropriate # tags that help ArgoCD do blue green deployments. previousServicesCount: "3" @@ -195,6 +195,7 @@ nodejs: limits: memory: "200Mi" cpu: "400m" + heapSize: 150 varnish: containerImage: diff --git a/node/server.js b/node/server.js index 682e7e811e..8fb190f6d9 100644 --- a/node/server.js +++ b/node/server.js @@ -86,7 +86,7 @@ const needsUpdating = function(cachekey, last_cached_to_compare){ const renderReaderApp = function(props, data, timer) { // Returns HTML of ReaderApp component given `props` and `data` - SefariaReact.sefariaSetup(data, props); //Do we really need to do Sefaria.setup every request? + SefariaReact.sefariaSetup(data, props, true); // true means reset cache - we are clearing out old data SefariaReact.unpackDataFromProps(props); timer.ms_to_set_data = timer.elapsed(); const html = ReactDOMServer.renderToString(ReaderApp(props)); diff --git a/reader/views.py b/reader/views.py index 8195e83b9d..a6e0e059fa 100644 --- a/reader/views.py +++ b/reader/views.py @@ -170,6 +170,7 @@ def base_props(request): if request.user.is_authenticated: profile = UserProfile(user_obj=request.user) + active_module = getattr(request, "active_module", "library") user_data = { "_uid": request.user.id, "_email": request.user.email, @@ -187,7 +188,7 @@ def base_props(request): "blocking": profile.blockees.uids, "calendars": get_todays_calendar_items(**_get_user_calendar_params(request)), "notificationCount": profile.unread_notification_count(), - "notifications": profile.recent_notifications().client_contents(), + "notifications": profile.recent_notifications(scope=active_module).client_contents(), "saved": {"loaded": False, "items": profile.get_history(saved=True, secondary=False, serialized=True, annotate=False)}, # saved is initially loaded without text annotations so it can quickly immediately mark any texts/sheets as saved, but marks as `loaded: false` so the full annotated data will be requested if the user visits the saved/history page "last_place": profile.get_history(last_place=True, secondary=False, sheets=False, serialized=True) } @@ -1075,7 +1076,8 @@ def user_stats(request): def notifications(request): # Notifications content is not rendered server side title = _("Sefaria Notifications") - notifications = UserProfile(user_obj=request.user).recent_notifications() + active_module = getattr(request, 'active_module', 'library') + notifications = UserProfile(user_obj=request.user).recent_notifications(scope=active_module) props = { "notifications": notifications.client_contents(), } @@ -2924,8 +2926,9 @@ def notifications_api(request): page = int(request.GET.get("page", 0)) page_size = int(request.GET.get("page_size", 10)) + scope = str(request.GET.get("scope", "library")) - notifications = NotificationSet().recent_for_user(request.user.id, limit=page_size, page=page) + notifications = NotificationSet().recent_for_user(request.user.id, limit=page_size, page=page, scope=scope) return jsonResponse({ "notifications": notifications.client_contents(), @@ -4472,6 +4475,7 @@ def annual_report(request, report_year): '2021': 'https://indd.adobe.com/embed/98a016a2-c4d1-4f06-97fa-ed8876de88cf?startpage=1&allowFullscreen=true', '2022': STATIC_URL + 'files/Sefaria_AnnualImpactReport_R14.pdf', '2023': 'https://issuu.com/sefariaimpact/docs/sefaria_2023_impact_report?fr=sMmRkNTcyMzMyNTk', + '2024': STATIC_URL + 'files/Sefaria_Impact_Report_2024.pdf' } # Assume the most recent year as default when one is not provided if not report_year: diff --git a/sefaria/helper/crm/crm_connection_manager.py b/sefaria/helper/crm/crm_connection_manager.py index acd022bb5f..e5332a5273 100644 --- a/sefaria/helper/crm/crm_connection_manager.py +++ b/sefaria/helper/crm/crm_connection_manager.py @@ -38,7 +38,7 @@ def mark_for_review_in_crm(self, crm_id): """ pass - def subscribe_to_lists(self, email, first_name=None, last_name=None, lang="en", educator=False): + def subscribe_to_lists(self, email, first_name=None, last_name=None, educator=False, lang="en"): CrmConnectionManager.validate_email(email) CrmConnectionManager.validate_name(first_name) CrmConnectionManager.validate_name(last_name) diff --git a/sefaria/helper/crm/dummy_crm.py b/sefaria/helper/crm/dummy_crm.py index 1bed1b8c56..328582401e 100644 --- a/sefaria/helper/crm/dummy_crm.py +++ b/sefaria/helper/crm/dummy_crm.py @@ -21,8 +21,8 @@ def mark_for_review_in_crm(self, crm_id): def change_user_email(self, uid, new_email): return True - def subscribe_to_lists(self, email, first_name=None, last_name=None, lang="en", educator=False): - CrmConnectionManager.subscribe_to_lists(self, email, first_name, last_name, lang, educator) + def subscribe_to_lists(self, email, first_name=None, last_name=None, educator=False, lang="en"): + CrmConnectionManager.subscribe_to_lists(self, email, first_name, last_name, educator, lang) return True def find_crm_id(self, email=None): diff --git a/sefaria/helper/crm/salesforce.py b/sefaria/helper/crm/salesforce.py index 8f040993f3..378c945549 100644 --- a/sefaria/helper/crm/salesforce.py +++ b/sefaria/helper/crm/salesforce.py @@ -130,13 +130,13 @@ def subscribe_to_lists( email: str, first_name: Optional[str] = None, last_name: Optional[str] = None, - lang: str = "en", educator: bool = False, + lang: str = "en", mailing_lists: Optional[list[str]] = None) -> Any: mailing_lists = mailing_lists or [] - CrmConnectionManager.subscribe_to_lists(self, email, first_name, last_name, lang, educator) + CrmConnectionManager.subscribe_to_lists(self, email, first_name, last_name, educator, lang) if lang == "he": language = "Hebrew" else: diff --git a/sefaria/model/notification.py b/sefaria/model/notification.py index ffe0b4e228..9009aef99d 100644 --- a/sefaria/model/notification.py +++ b/sefaria/model/notification.py @@ -171,6 +171,13 @@ class Notification(abst.AbstractMongoRecord): "suspected_spam" ] + sheets_notification_types = [ + "collection add", + "follow", + "sheet like", + "sheet publish" + ] + def _init_defaults(self): self.read = False self.read_via = None @@ -326,7 +333,6 @@ def __init__(self, query=None, page=0, limit=0, sort=[["date", -1]]): def _add_global_messages(self, uid): """ Add user Notification records for any new GlobalNotifications - :return: """ latest_id_for_user = Notification.latest_global_for_user(uid) latest_global_id = GlobalNotification.latest_id() @@ -336,28 +342,44 @@ def _add_global_messages(self, uid): else: GlobalNotificationSet({"_id": {"$gt": latest_id_for_user}}, limit=10).register_for_user(uid) - def unread_for_user(self, uid): + def _build_query_with_scope(self, uid, read=None, is_global=None, suspected_spam=None, scope='library'): + """ + Helper method to build a query with the given parameters and scope. + """ + query = {"uid": uid} + if read is not None: + query["read"] = read + if is_global is not None: + query["is_global"] = is_global + if suspected_spam is not None: + query["suspected_spam"] = suspected_spam + query["type"] = {"$in" if scope == 'sheets' else "$nin": Notification.sheets_notification_types} + return query + + def unread_for_user(self, uid, scope='library'): """ Loads the unread notifications for uid. """ - # Add globals ... self._add_global_messages(uid) - self.__init__(query={"uid": uid, "read": False}) + query = self._build_query_with_scope(uid, read=False, scope=scope) + self.__init__(query=query) return self - def unread_personal_for_user(self, uid): + def unread_personal_for_user(self, uid, scope='library'): """ - Loads the unread notifications for uid. + Loads the unread personal notifications for uid. """ - self.__init__(query={"uid": uid, "read": False, "is_global": False, "suspected_spam": {'$in': [False, None]}}) + query = self._build_query_with_scope(uid, read=False, is_global=False, suspected_spam={'$in': [False, None]}, scope=scope) + self.__init__(query=query) return self - def recent_for_user(self, uid, page=0, limit=10): + def recent_for_user(self, uid, page=0, limit=10, scope='library'): """ Loads recent notifications for uid. """ self._add_global_messages(uid) - self.__init__(query={"uid": uid, "suspected_spam": {'$in': [False, None]}}, page=page, limit=limit) + query = self._build_query_with_scope(uid, suspected_spam={"$in": [False, None]}, scope=scope) + self.__init__(query=query, page=page, limit=limit) return self def mark_read(self, via="site"): diff --git a/sefaria/model/text.py b/sefaria/model/text.py index 0032f96b2f..7d50655ae3 100644 --- a/sefaria/model/text.py +++ b/sefaria/model/text.py @@ -4721,7 +4721,7 @@ def url(self): :return string: normal url form """ if not self._url: - self._url = self.normal().replace(" ", "_").replace(":", ".") + self._url = self.normal().replace(" ", "_").replace(":", ".").replace("?", "%3F") # Change "Mishna_Brachot_2:3" to "Mishna_Brachot.2.3", but don't run on "Mishna_Brachot" if len(self.sections) > 0: diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py index ac3a8e2d70..a403f6fee7 100644 --- a/sefaria/model/topic.py +++ b/sefaria/model/topic.py @@ -64,7 +64,7 @@ def get_title(self, lang): return self._index.get_title(lang) def get_url(self): - return f'/{self._index.title.replace(" ", "_")}' + return f'/{self._index.title.replace(" ", "_").replace("?", "%3F")}' class AuthorCategoryAggregation(AuthorWorksAggregation): diff --git a/sefaria/model/user_profile.py b/sefaria/model/user_profile.py index c8ff09a7ac..fffa9143c6 100644 --- a/sefaria/model/user_profile.py +++ b/sefaria/model/user_profile.py @@ -407,6 +407,8 @@ def __init__(self, user_obj=None, id=None, slug=None, email=None, user_registrat # If we encounter a user that has a Django user record but not a profile document # create a profile for them. This allows two enviornments to share a user database, # while maintaining separate profiles (e.g. Sefaria and S4D). + self.show_editor_toggle = False + self.uses_new_editor = True self.assign_slug() self.save() @@ -592,13 +594,14 @@ def followed_by(self, uid): """Returns true if this user is followed by uid""" return uid in self.followers.uids - def recent_notifications(self): + def recent_notifications(self, scope="library"): from sefaria.model.notification import NotificationSet - return NotificationSet().recent_for_user(self.id) + return NotificationSet().recent_for_user(self.id, scope=scope) - def unread_notification_count(self): + def unread_notification_count(self, scope="library"): + # TODO: Why do we not need to scope the notifications to the module here? from sefaria.model.notification import NotificationSet - return NotificationSet().unread_for_user(self.id).count() + return NotificationSet().unread_for_user(self.id, scope=scope).count() def process_history_item(self, hist, time_stamp): action = hist.pop("action", None) diff --git a/sefaria/settings_utils.py b/sefaria/settings_utils.py index c5d330d296..01b06e0d92 100644 --- a/sefaria/settings_utils.py +++ b/sefaria/settings_utils.py @@ -23,8 +23,8 @@ def before_send(event, hint): dsn=sentry_dsn, environment=sentry_environment, integrations=[DjangoIntegration()], - traces_sample_rate=0.1, - profiles_sample_rate=0.1, + traces_sample_rate=0.01, + profiles_sample_rate=0.01, send_default_pii=False, before_send=before_send, max_breadcrumbs=30, diff --git a/sefaria/urls.py b/sefaria/urls.py index 29ebbd7058..0d878c48ab 100644 --- a/sefaria/urls.py +++ b/sefaria/urls.py @@ -47,6 +47,7 @@ url(r'^translations/(?P[^.]+)$', reader_views.translations_page), url(r'^community/?$', reader_views.community_page), url(r'^notifications/?$', reader_views.notifications), + url(r'^sheets/notifications/?$', reader_views.notifications), url(r'^modtools/?$', reader_views.modtools), url(r'^modtools/upload_text$', sefaria_views.modtools_upload_workflowy), url(r'^modtools/links$', sefaria_views.links_upload_api), diff --git a/static/css/s2.css b/static/css/s2.css index dfb5a6c7db..76d7d3c521 100644 --- a/static/css/s2.css +++ b/static/css/s2.css @@ -8378,14 +8378,14 @@ But not to use a display block directive that might break continuous mode for ot } .notification .sheetTitle, .notification .collectionName { - --english-font: var(--english-serif-font-family); - --hebrew-font: var(--hebrew-serif-font-family); + --english-font: var(--english-sans-serif-font-family); + --hebrew-font: var(--hebrew-sans-serif-font-family); font-size: 24px; display: block; } .notification .sheetSummary { - --english-font: var(--english-serif-font-family); - --hebrew-font: var(--hebrew-serif-font-family); + --english-font: var(--english-sans-serif-font-family); + --hebrew-font: var(--hebrew-sans-serif-font-family); color: var(--dark-grey); font-size: 18px; margin-top: 10px; @@ -15279,6 +15279,10 @@ span.ref-link-color-3 {color: blue} margin-top: 30px; } +.notificationUserName { + color: var(--midrash-green) !important; +} + .emptyNotificationsTitle{ font-family: Roboto; font-size: 16px; diff --git a/static/files/Sefaria_Impact_Report_2024.pdf b/static/files/Sefaria_Impact_Report_2024.pdf new file mode 100644 index 0000000000..edc2e95bf9 Binary files /dev/null and b/static/files/Sefaria_Impact_Report_2024.pdf differ diff --git a/static/js/ConnectionsPanel.jsx b/static/js/ConnectionsPanel.jsx index c4ddcd3907..c31abf235b 100644 --- a/static/js/ConnectionsPanel.jsx +++ b/static/js/ConnectionsPanel.jsx @@ -1133,19 +1133,6 @@ class ShareBox extends Component { - {this.state.sheet && Sefaria._uid === this.state.sheet.owner ? -
- People with this link can - -
: null} diff --git a/static/js/HeaderAutocomplete.jsx b/static/js/HeaderAutocomplete.jsx index 6a1b757aaa..3195115d59 100644 --- a/static/js/HeaderAutocomplete.jsx +++ b/static/js/HeaderAutocomplete.jsx @@ -147,6 +147,7 @@ const SearchSuggestionInner = ({ value, type, displayedLabel, label, url, pic, wrapperClasses, universalIndex, highlightedIndex, getItemProps, onClick}) => { const isHebrew = Sefaria.hebrew.isHebrew(label); + url = url?.replace(/\?/g, '%3F'); return (
{ Sefaria.track.event("Search", `Search Box Navigation - ${item.type}`, item.key); clearSearchBox(onChange); - const url = item.url + const url = item.url.replace(/\?/g, '%3F'); const handled = openURL(url); if (!handled) { window.location = url; diff --git a/static/js/NotificationsPanel.jsx b/static/js/NotificationsPanel.jsx index 2328618e82..f8e71e5b41 100644 --- a/static/js/NotificationsPanel.jsx +++ b/static/js/NotificationsPanel.jsx @@ -53,7 +53,8 @@ class NotificationsPanel extends Component { } } getMoreNotifications() { - $.getJSON("/api/notifications?page=" + this.state.page, this.loadMoreNotifications); + const activeModule = Sefaria.activeModule; + $.getJSON(`/api/notifications?page=${this.state.page}&scope=${activeModule}`, this.loadMoreNotifications); this.setState({loading: true}); } loadMoreNotifications(data) { @@ -81,13 +82,10 @@ class NotificationsPanel extends Component { Notifications
- {(Sefaria._uid) ? ( - Sefaria.notificationCount > 0 && notifications - ) : ( - - )} + {!Sefaria._uid && } - {Sefaria._uid && Sefaria.notificationCount < 1 && } + {Sefaria._uid && notifications.length < 1 && } + {Sefaria._uid && notifications.length > 0 && notifications} @@ -167,7 +165,7 @@ const Notification = ({imageUrl, imageLink, topLine, date, body}) => { const SheetPublishNotification = ({date, content}) => { const topLine = ( <> -
{content.name}  + {content.name}  published a new sheet ); @@ -197,7 +195,7 @@ const SheetPublishNotification = ({date, content}) => { const SheetLikeNotification = ({date, content}) => { const topLine = ( <> - {content.name}  + {content.name}  liked your sheet ); @@ -223,7 +221,7 @@ const FollowNotification = ({date, content}) => { const topLine = ( <> - {content.name}  + {content.name}  is now following you ); diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index af1006fd84..2cd9da2f78 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -772,6 +772,9 @@ class ReaderApp extends Component { } } } + // Replace question marks that can be included in titles + // (not using encodeURIComponent for this can run twice and encode the % of the first running) + hist.url = hist.url.replace(/\?/g, '%3F') // Replace the first only & with a ? hist.url = hist.url.replace(/&/, "?"); @@ -1162,13 +1165,14 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) { } else if (path.match(/^\/translations\/.+/)) { let slug = path.slice(14); this.openTranslationsPage(slug); - } else if (Sefaria.isRef(path.slice(1))) { + } else if (Sefaria.isRef(path.slice(1).replace(/%3F/g, '?'))) { + const ref = path.slice(1).replace(/%3F/g, '?'); const currVersions = { en: Sefaria.util.getObjectFromUrlParam(params.get("ven")), he: Sefaria.util.getObjectFromUrlParam(params.get("vhe")) }; - const options = {showHighlight: path.slice(1).indexOf("-") !== -1}; // showHighlight when ref is ranged - openPanel(Sefaria.humanRef(path.slice(1)), currVersions, options); + const options = {showHighlight: ref.indexOf("-") !== -1}; // showHighlight when ref is ranged + openPanel(Sefaria.humanRef(ref), currVersions, options); } else { return false } diff --git a/static/js/TextList.jsx b/static/js/TextList.jsx index 9d30cb00c9..29acbc3f88 100644 --- a/static/js/TextList.jsx +++ b/static/js/TextList.jsx @@ -21,6 +21,7 @@ class TextList extends Component { linksLoaded: false, // has the list of refs been loaded textLoaded: false, // has the text of those refs been loaded waitForText: true, // should we delay rendering texts until preload is finished + links: [] } } componentDidMount() { @@ -30,15 +31,10 @@ class TextList extends Component { componentWillUnmount() { this._isMounted = false; } - componentWillReceiveProps(nextProps) { - if (!Sefaria.util.object_equals(this.props.filter, nextProps.filter)) { - this.preloadText(nextProps.filter); - } - } componentWillUpdate(nextProps) { } componentDidUpdate(prevProps, prevState) { - if (!prevProps.srefs.compare(this.props.srefs)) { + if (!prevProps.srefs.compare(this.props.srefs) || !Sefaria.util.object_equals(this.props.filter, prevProps.filter)) { this.loadConnections(); } const didRender = prevState.linksLoaded && (!prevState.waitForText || prevState.textLoaded); @@ -49,19 +45,22 @@ class TextList extends Component { } } getSectionRef() { - var ref = this.props.srefs[0]; // TODO account for selections spanning sections - var sectionRef = Sefaria.sectionRef(ref, true) || ref; + const ref = this.props.srefs[0]; // TODO account for selections spanning sections + const sectionRef = Sefaria.sectionRef(ref, true) || ref; return sectionRef; } loadConnections() { - // Load connections data from server for this section - var sectionRef = this.getSectionRef(); + // Load connections data from server for this section, and updates links based on current filter and current ref + // Finally, preload commentary texts so that it's ready when the user clicks to open a connections panel link in a main panel + const sectionRef = this.getSectionRef(); if (!sectionRef) { return; } Sefaria.related(sectionRef, function(data) { if (this._isMounted) { - this.preloadText(this.props.filter); this.setState({ linksLoaded: true, + links: this.getLinksAndFilter() + }, () => { + this.preloadCommentaryText(this.props.filter); }); } }.bind(this)); @@ -71,98 +70,38 @@ class TextList extends Component { this.setState({linksLoaded: false}); this.loadConnections(); } - preloadText(filter) { - // Preload text of links if `filter` is a single commentary, or all commentary - if (filter.length == 1 && - Sefaria.index(filter[0]) && // filterSuffix for quoting commmentary prevents this path for QC - (Sefaria.index(filter[0]).categories[0] == "Commentary"|| - Sefaria.index(filter[0]).primary_category == "Commentary")) { - // Individual commentator names ("Rashi") are put into Sefaria.index with "Commentary" as first category - // Intentionally fails when looking up "Rashi on Genesis", which indicates we're looking at a quoting commentary. - this.preloadSingleCommentaryText(filter); - - } else if (filter.length == 1 && filter[0] == "Commentary") { - this.preloadAllCommentaryText(filter); - - } else { - this.setState({waitForText: false, textLoaded: false}); - } - } - preloadSingleCommentaryText(filter) { - //console.log('preloading single commentary') - // Preload commentary for an entire section of text. - this.setState({textLoaded: false}); - var commentator = filter[0]; - var basetext = this.getSectionRef(); - var commentarySection = Sefaria.commentarySectionRef(commentator, basetext); - if (!commentarySection) { - this.setState({waitForText: false}); - return; - } - this.setState({waitForText: true}); - Sefaria.text(commentarySection, {}, function() { - if (this._isMounted) { - this.setState({textLoaded: true}); - } - }.bind(this)); - } - preloadAllCommentaryText() { - var basetext = this.getSectionRef(); - var summary = Sefaria.linkSummary(basetext); - if (summary.length && summary[0].category == "Commentary") { - this.setState({textLoaded: false, waitForText: true}); - // Get a list of commentators on this section that we need don't have in the cache - var commentators = summary[0].books.map(function(item) { - return item.book; - }); - - if (commentators.length) { - var commentarySections = commentators.map(function(commentator) { - return Sefaria.commentarySectionRef(commentator, basetext); - }).filter(function(commentarySection) { - return !!commentarySection; + preloadCommentaryText(filter) { + // Preload text of links if `filter` is a single commentary + // Terms like "Rashi" or "Ibn Ezra" are currently in the Sefaria._index cache. + // categories[0] === "Commentary" only for these terms, whereas a real index like "Rashi on Genesis" has categories[0] === "Tanakh". + // We don't want to include "Rashi on Genesis" case because that's a "Quoting Commentary" case. + const isOneText = filter.length === 1 && Sefaria.index(filter[0]); + const isCommentary = Sefaria.index(filter[0])?.primary_category === "Commentary" || Sefaria.index(filter[0])?.categories[0] === "Commentary"; + if (isCommentary && isOneText) { + this.setState({waitForText: true}); + // Get the refs of the links and zoom out one level. In most cases, the refs will be the same, so it's helpful to use a Set. + const refs = [...new Set(this.state.links.map(link => Sefaria.humanRef(Sefaria.zoomOutRef(link.sourceRef))))]; + refs.map(ref => { + Sefaria.getTextFromCurrVersions(ref, {}, this.props.translationLanguagePreference, false).then(data =>{ + this.setState({textLoaded: true, waitForText: false}); }); - this.waitingFor = Sefaria.util.clone(commentarySections); - this.target = 0; - for (var i = 0; i < commentarySections.length; i++) { - Sefaria.text(commentarySections[i], {}, function(data) { - var index = this.waitingFor.indexOf(data.commentator); - if (index == -1) { - // console.log("Failed to clear commentator:"); - // console.log(data); - this.target += 1; - } - if (index > -1) { - this.waitingFor.splice(index, 1); - } - if (this.waitingFor.length == this.target) { - if (this._isMounted) { - this.setState({textLoaded: true}); - } - } - }.bind(this)); - } - } else { - // All commentaries have been loaded already - this.setState({textLoaded: true}); - } + }); } else { - // There were no commentaries to load - this.setState({textLoaded: true}); + this.setState({waitForText: false, textLoaded: false}); } } - getLinks() { - var refs = this.props.srefs; - var filter = this.props.filter; - var excludedSheet = this.props.nodeRef ? this.props.nodeRef.split(".")[0] : null; - var sectionRef = this.getSectionRef(); + getLinksAndFilter() { + const refs = this.props.srefs; + const filter = this.props.filter; + const excludedSheet = this.props.nodeRef ? this.props.nodeRef.split(".")[0] : null; + const sectionRef = this.getSectionRef(); - var sortConnections = function(a, b) { + const sortConnections = function(a, b) { // Sort according this which verse the link connects to if (a.anchorVerse !== b.anchorVerse) { return a.anchorVerse - b.anchorVerse; } - if (a.index_title == b.index_title) { + if (a.index_title === b.index_title) { // For Sheet links of the same group sort by title if (a.isSheet && b.isSheet) { return a.title > b.title ? 1 : -1; @@ -170,7 +109,7 @@ class TextList extends Component { // For text links of same text/commentary use content order, set by server return a.commentaryNum - b.commentaryNum; } - if (this.props.contentLang == "hebrew") { + if (this.props.contentLang === "hebrew") { return a.sourceHeRef > b.sourceHeRef ? 1 : -1; } else { return a.sourceRef > b.sourceRef ? 1 : -1; @@ -198,16 +137,15 @@ class TextList extends Component { var oref = Sefaria.ref(refs[0]); var filter = this.props.filter; // Remove filterSuffix for display var displayFilter = filter.map(filter => filter.split("|")[0]); // Remove filterSuffix for display - var links = this.getLinks(); var en = "No connections known" + (filter.length ? " for " + displayFilter.join(", ") + " here" : "") + "."; var he = "אין קשרים ידועים" + (filter.length ? " ל" + displayFilter.map(f => Sefaria.hebrewTerm(f)).join(", ") : "") + "."; var noResultsMessage = ; - var message = !this.state.linksLoaded ? () : (links.length === 0 ? noResultsMessage : null); - var content = links.length === 0 ? message : + var message = !this.state.linksLoaded ? () : (this.state.links.length === 0 ? noResultsMessage : null); + var content = this.state.links.length === 0 ? message : this.state.waitForText && !this.state.textLoaded ? () : - links.map(function(link, i) { + this.state.links.map(function(link, i) { if (link.isSheet) { var hideAuthor = link.index_title == this.props.filter[0]; return ( "Rashi on Genesis 3" - // Even though most commentaries have a 1:1 structural match to basetexts, this is not alway so. - // Works by examining links available on baseRef, returns null if no links are in cache. - if (commentator == "Abarbanel") { - return null; // This text is too giant, optimizing up to section level is too slow. TODO: generalize. - } - var links = Sefaria.getLinksFromCache(baseRef); - links = Sefaria._filterLinks(links, [commentator]); - if (!links || !links.length || links[0].isSheet) { return null; } - - var pRefs = links.map(link => Sefaria.parseRef(link.sourceRef)); - if (pRefs.some(pRef => "error" in pRef)) { return null; } // Give up on bad refs - - var books = pRefs.map(pRef => pRef.book).unique(); - if (books.length > 1) { return null; } // Can't handle multiple index titles or schemaNodes - - try { - var startSections = pRefs.map(pRef => pRef.sections[0]); - var endSections = pRefs.map(pRef => pRef.toSections[0]); - } catch(e) { - return null; - } - - const sorter = (a, b) => { - return a.match(/\d+[ab]/) ? - Sefaria.hebrew.dafToInt(a) - Sefaria.hebrew.dafToInt(b) - : parseInt(a) - parseInt(b); - }; - - var commentaryRef = { - book: books[0], - sections: startSections.sort(sorter).slice(0,1), - toSections: endSections.sort(sorter).reverse().slice(0,1) - }; - var ref = Sefaria.humanRef(Sefaria.makeRef(commentaryRef)); - - return ref; - }, - _descDict: {}, // cache for the description dictionary - getDescriptions: function(keyName, categoryList) { + _descDict: {}, // cache for the description dictionary + getDescriptions: function(keyName, categoryList) { const catlist = Sefaria.tocItemsByCategories(categoryList) let catmap = catlist.map((e) => [e.category || e.title, e.enShortDesc, e.heShortDesc]) let d = {} @@ -1842,7 +1803,7 @@ Sefaria = extend(Sefaria, { let heShortDesc = descs && descs[1]? descs[1]: null; return [enShortDesc, heShortDesc]; }, - getDescriptionDict: function(keyName, categoryList){ + getDescriptionDict: function(keyName, categoryList){ let desc = this._cachedApi([keyName, categoryList], this._descDict, null); if (Object.keys(this._descDict).length === 0){ //Init of the Dict with the Category level descriptions @@ -1865,9 +1826,8 @@ Sefaria = extend(Sefaria, { { return [null, null]; } - }, - - _notes: {}, + }, + _notes: {}, notes: function(ref, callback) { var notes = null; if (typeof ref == "string") { @@ -2813,10 +2773,6 @@ _media: {}, _tableOfContentsDedications: {}, _strapiContent: null, _inAppAds: null, - _stories: { - stories: [], - page: 0 - }, _upcomingDay: {}, // for example, possible keys are 'parasha' and 'holiday' getUpcomingDay: function(day) { // currently `day` can be 'holiday' or 'parasha' @@ -3632,7 +3588,10 @@ Sefaria.palette.refColor = ref => Sefaria.palette.indexColor(Sefaria.parseRef(re Sefaria = extend(Sefaria, Strings); -Sefaria.setup = function(data, props = null) { +Sefaria.setup = function(data, props = null, resetCache = false) { + if (resetCache) { + Sefaria.resetCache(); + } Sefaria.loadServerData(data); let baseProps = props !=null ? props : (typeof DJANGO_VARS === "undefined" ? undefined : DJANGO_VARS.props); Sefaria.unpackBaseProps(baseProps); @@ -3651,5 +3610,64 @@ Sefaria.setup = function(data, props = null) { }; Sefaria.setup(); +Sefaria.resetCache = function() { + // Used when site is run in a server context for SSR. + // Clears out caches that are intended for browser rendering, and resets system to clean state. + + // Caches that are user-level or can grow unbounded + this.last_place = []; // Code smell: user state stored in library + this._parseRef = {}; + this._texts = {}; + this._textsStore = {}; + this._refmap = {}; + this._bulkTexts = {}; + this._bulkSheets = {}; + this._versions = {}; + this._translateVersions = {}; + this._shape = {}; + this._lookups = {}; + this._lexiconCompletions = {}; + this._lexiconLookups = {}; + this._links = {}; + this._linkSummaries = {}; + this._notes = {}; + this._privateNotes = {}; + this._media = {}; + this._webpages = {}; + this._processedWebpages = {}; + this._refTopicLinks = {}; + this._related = {}; + this._relatedPrivate = {}; + this._manuscripts = {}; + this._guides = {}; + this._profiles = {}; + this._topics = {}; + this._translations = {}; + this._collections = {}; + this._collectionsList = {}; + this._userCollections = {}; + this._userCollectionsForSheet = {}; + this._ajaxObjects = {}; + this._i18nInterfaceStringsWithContext = {}; // Not sure about this one. May be retainable. + this._siteSettings = {}; // Where does this get set? + + // These change slowly, but they do change + this._inAppAds = {}; + this._upcomingDay = {}; + this._parashaNextRead = {}; + this._featuredTopic = {}; + this._seasonalTopic = {}; + this._index = {}; + this._indexDetails = {}; + this._bookSearchPathFilter = {}; + this.booksDict = {}; // This gets built from setup, via _makeBooksDict + this._tocOrderLookup = {}; // This gets built from setup, via _cacheFromToc + this._translateTerms = {}; // This gets built from setup, via _cacheHebrewTerms + this._i18nInterfaceStrings = {}; // This gets built from setup, via _cacheSiteInterfaceStrings + this._descDict = {}; // Stays constant + this._TopicsByPool = {}; // constant + this._portals = {}; // constant + this._tableOfContentsDedications = {}; +}; export default Sefaria; diff --git a/static/js/sefaria/strings.js b/static/js/sefaria/strings.js index 4ae7d30a26..a780f34501 100644 --- a/static/js/sefaria/strings.js +++ b/static/js/sefaria/strings.js @@ -407,6 +407,7 @@ const Strings = { "Portuguese": "פורטוגזית", "Spanish": "ספרדית", "French": "צרפתית", + "Romanian": "רומנית", "German": "גרמנית", "Arabic": "ערבית", "Italian": "איטלקית",